From c528a379870a53b73f52a2ae5f7aefc6984c16c0 Mon Sep 17 00:00:00 2001 From: busya Date: Fri, 29 May 2026 22:16:39 +0300 Subject: [PATCH] agents ai native front --- .axiom/semantic_index/index.json | 26300 ++++++++++--------- .opencode/agents/fullstack-coder.md | 27 +- .opencode/agents/qa-tester.md | 41 +- .opencode/agents/svelte-coder.md | 44 +- .opencode/skills/semantics-core/SKILL.md | 4 +- .opencode/skills/semantics-svelte/SKILL.md | 244 +- 6 files changed, 14167 insertions(+), 12493 deletions(-) diff --git a/.axiom/semantic_index/index.json b/.axiom/semantic_index/index.json index af8636c9..879cb6f4 100644 --- a/.axiom/semantic_index/index.json +++ b/.axiom/semantic_index/index.json @@ -1,10 +1,28 @@ { - "generated_at": "2026-05-26T13:03:36.710236723Z", + "generated_at": "2026-05-29T19:13:42.584425298Z", "root_path": "/home/busya/dev/ss-tools", - "contract_count": 3299, - "edge_count": 2156, - "file_count": 694, + "contract_count": 3365, + "edge_count": 2187, + "file_count": 716, "contracts": [ + { + "contract_id": "AlembicEnvModule", + "contract_type": "Module", + "file_path": "backend/alembic/env.py", + "start_line": 1, + "end_line": 8, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Alembic environment configuration — sets up DB connection, model metadata,", + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AlembicEnvModule [C:3] [TYPE Module] [SEMANTICS alembic, migration, env, logging]\n# @BRIEF Alembic environment configuration — sets up DB connection, model metadata,\n# and logging. Contains ADR [LOG-001] for suppress_existing_loggers=False.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @INVARIANT fileConfig() must be called with disable_existing_loggers=False\n# to prevent disabling superset_tools_app logger (see ADR LOG-001).\n# #endregion AlembicEnvModule\n" + }, { "contract_id": "SrcRoot", "contract_type": "Module", @@ -332,7 +350,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/__tests__/conftest.py", "start_line": 1, - "end_line": 37, + "end_line": 38, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -343,7 +361,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region RoutesTestsConftest [TYPE Module] [C:1] [SEMANTICS test, fixture, mock, conftest]\n# @BRIEF Shared low-fidelity test doubles for API route test modules.\n\n# Set required env vars before any app imports\nimport os\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", \"sqlite:///:memory:test_auth\")\nos.environ.setdefault(\"DEV_MODE\", \"true\")\nclass FakeQuery:\n \"\"\"Shared chainable query stub for route tests.\n WARNING: filter() is predicate-blind — all ownership and permission filters are\n ignored. Tests using FakeQuery cannot verify scoped data access. This is a\n known limitation; do not use for permission-sensitive test paths without a\n spec-guarded replacement.\n \"\"\"\n def __init__(self, rows):\n self._rows = list(rows)\n self._seen_predicates = []\n def filter(self, *args, **kwargs):\n # Predicate-aware bookkeeping only; no predicate evaluation is performed.\n self._seen_predicates.append((args, kwargs))\n return self\n def order_by(self, *args, **kwargs):\n return self\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n def first(self):\n return self._rows[0] if self._rows else None\n def all(self):\n return list(self._rows)\n def count(self):\n return len(self._rows)\n# #endregion RoutesTestsConftest\n" + "body": "# #region RoutesTestsConftest [TYPE Module] [C:1] [SEMANTICS test, fixture, mock, conftest]\n# @BRIEF Shared low-fidelity test doubles for API route test modules.\n\n# Set required env vars before any app imports\nimport os\n\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", \"sqlite:///:memory:test_auth\")\nos.environ.setdefault(\"DEV_MODE\", \"true\")\nclass FakeQuery:\n \"\"\"Shared chainable query stub for route tests.\n WARNING: filter() is predicate-blind — all ownership and permission filters are\n ignored. Tests using FakeQuery cannot verify scoped data access. This is a\n known limitation; do not use for permission-sensitive test paths without a\n spec-guarded replacement.\n \"\"\"\n def __init__(self, rows):\n self._rows = list(rows)\n self._seen_predicates = []\n def filter(self, *args, **kwargs):\n # Predicate-aware bookkeeping only; no predicate evaluation is performed.\n self._seen_predicates.append((args, kwargs))\n return self\n def order_by(self, *args, **kwargs):\n return self\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n def first(self):\n return self._rows[0] if self._rows else None\n def all(self):\n return list(self._rows)\n def count(self):\n return len(self._rows)\n# #endregion RoutesTestsConftest\n" }, { "contract_id": "AssistantApiTests", @@ -369,7 +387,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]\n# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.\n# @RELATION DEPENDS_ON -> [AssistantApi]\n# @INVARIANT Every test clears assistant in-memory state before execution.\nimport asyncio\nfrom datetime import datetime\nfrom unittest.mock import MagicMock\nimport uuid\n\nfrom src.api.routes import assistant as assistant_routes\nfrom src.models.assistant import AssistantMessageRecord\nfrom src.models.dataset_review import (\n ApprovalState,\n CandidateStatus,\n DatasetReviewSession,\n ExecutionMapping,\n ImportedFilter,\n MappingMethod,\n ReadinessState,\n RecommendedAction,\n SemanticCandidate,\n SemanticFieldEntry,\n SessionPhase,\n SessionStatus,\n)\nfrom src.schemas.auth import User\n\n\n# #region _run_async [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _run_async(coro):\n return asyncio.run(coro)\n# #endregion _run_async\n# #region _FakeTask [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests.\n# @INVARIANT status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.\nclass _FakeTask:\n def __init__(\n self,\n id,\n status=\"SUCCESS\",\n plugin_id=\"unknown\",\n params=None,\n result=None,\n user_id=None,\n ):\n self.id = id\n self.status = status\n self.plugin_id = plugin_id\n self.params = params or {}\n self.result = result or {}\n self.user_id = user_id\n self.started_at = datetime.utcnow()\n self.finished_at = datetime.utcnow()\n# #endregion _FakeTask\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF In-memory task manager stub that records created tasks for route-level assertions.\n# @INVARIANT create_task stores tasks retrievable by get_task/get_tasks without external side effects.\nclass _FakeTaskManager:\n def __init__(self):\n self.tasks = {}\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{uuid.uuid4().hex[:8]}\"\n task = _FakeTask(\n task_id,\n status=\"STARTED\",\n plugin_id=plugin_id,\n params=params,\n user_id=user_id,\n )\n self.tasks[task_id] = task\n return task\n def get_task(self, task_id):\n return self.tasks.get(task_id)\n def get_tasks(self, limit=20, offset=0):\n return sorted(self.tasks.values(), key=lambda t: t.id, reverse=True)[\n offset : offset + limit\n ]\n def get_all_tasks(self):\n return list(self.tasks.values())\n# #endregion _FakeTaskManager\n# #region _FakeConfigManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests.\n# @INVARIANT get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.\nclass _FakeConfigManager:\n class _Env:\n def __init__(self, id, name):\n self.id = id\n self.name = name\n def get_environments(self):\n return [self._Env(\"dev\", \"Development\"), self._Env(\"prod\", \"Production\")]\n def get_config(self):\n class _Settings:\n default_environment_id = \"dev\"\n llm = {}\n class _Config:\n settings = _Settings()\n environments = []\n return _Config()\n# #endregion _FakeConfigManager\n# #region _admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build admin principal with spec=User for assistant route authorization tests.\ndef _admin_user():\n user = MagicMock(spec=User)\n user.id = \"u-admin\"\n user.username = \"admin\"\n role = MagicMock()\n role.name = \"Admin\"\n user.roles = [role]\n return user\n# #endregion _admin_user\n# #region _limited_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build limited user principal with empty roles for assistant route denial tests.\ndef _limited_user():\n user = MagicMock(spec=User)\n user.id = \"u-limited\"\n user.username = \"limited\"\n user.roles = []\n return user\n# #endregion _limited_user\n# #region _FakeQuery [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths.\n# @INVARIANT filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.\nclass _FakeQuery:\n def __init__(self, items):\n self.items = items\n def outerjoin(self, *args, **kwargs):\n return self\n def options(self, *args, **kwargs):\n return self\n def filter(self, *args, **kwargs):\n # @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n def order_by(self, *args, **kwargs):\n return self\n def limit(self, n):\n self.items = self.items[:n]\n return self\n def offset(self, n):\n self.items = self.items[n:]\n return self\n def first(self):\n return self.items[0] if self.items else None\n def all(self):\n return self.items\n def count(self):\n return len(self.items)\n# #endregion _FakeQuery\n# #region _FakeDb [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Explicit in-memory DB session double limited to assistant message persistence paths.\n# @INVARIANT query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.\nclass _FakeDb:\n def __init__(self):\n self.added = []\n self.dataset_sessions = {}\n def query(self, model):\n if model == AssistantMessageRecord:\n return _FakeQuery([])\n if model == DatasetReviewSession:\n return _FakeQuery(list(self.dataset_sessions.values()))\n return _FakeQuery([])\n def add(self, obj):\n self.added.append(obj)\n def commit(self):\n pass\n def rollback(self):\n pass\n def merge(self, obj):\n return obj\n def refresh(self, obj):\n pass\n# #endregion _FakeDb\n# #region _clear_assistant_state [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _clear_assistant_state():\n assistant_routes.CONVERSATIONS.clear()\n assistant_routes.USER_ACTIVE_CONVERSATION.clear()\n assistant_routes.CONFIRMATIONS.clear()\n assistant_routes.ASSISTANT_AUDIT.clear()\n# #endregion _clear_assistant_state\n# #region _dataset_review_session [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n last_activity_at=datetime.utcnow(),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n# #endregion _dataset_review_session\n# #region _await_none [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Async helper returning None for planner fallback tests.\nasync def _await_none(*args, **kwargs):\n return None\n# #endregion _await_none\n# #region test_unknown_command_returns_needs_clarification [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Unknown command should return clarification state and unknown intent.\ndef test_unknown_command_returns_needs_clarification(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"some random gibberish\")\n # We mock LLM planner to return low confidence\n monkeypatch.setattr(assistant_routes, \"_plan_intent_with_llm\", lambda *a, **k: None)\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"needs_clarification\"\n assert \"уточните\" in resp.text.lower() or \"неоднозначна\" in resp.text.lower()\n# #endregion test_unknown_command_returns_needs_clarification\n# #region test_capabilities_question_returns_successful_help [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Capability query should return deterministic help response.\ndef test_capabilities_question_returns_successful_help(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"что ты умеешь?\")\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"success\"\n assert \"я могу сделать\" in resp.text.lower()\n# #endregion test_capabilities_question_returns_successful_help\n# #region test_assistant_message_request_accepts_dataset_review_session_binding [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Assistant request schema should accept active dataset review session binding for scoped orchestration.\ndef test_assistant_message_request_accepts_dataset_review_session_binding():\n request = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assert request.dataset_review_session_id == \"sess-1\"\n# #endregion test_assistant_message_request_accepts_dataset_review_session_binding\n# #region test_dataset_review_scoped_message_uses_masked_filter_context [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted.\ndef test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch):\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"show filters\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n async def _fake_dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n ):\n return str(intent[\"entities\"][\"summary\"]), None, []\n monkeypatch.setattr(\n assistant_routes,\n \"_dispatch_dataset_review_intent\",\n _fake_dispatch_dataset_review_intent,\n )\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"success\"\n persisted_assistant = [\n item for item in db.added if getattr(item, \"role\", None) == \"assistant\"\n ][-1]\n imported_filters = persisted_assistant.payload[\"dataset_review_context\"][\n \"imported_filters\"\n ]\n assert imported_filters[0][\"raw_value\"] == \"***@example.com\"\n assert imported_filters[0][\"raw_value_masked\"] is True\n# #endregion test_dataset_review_scoped_message_uses_masked_filter_context\n# #region test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata.\ndef test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_approve_mappings\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"session_version\"] == 3\n assert resp.intent[\"entities\"][\"mapping_ids\"] == [\"map-1\"]\n# #endregion test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval\n# #region test_dataset_review_scoped_command_routes_field_semantics_update [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata.\ndef test_dataset_review_scoped_command_routes_field_semantics_update():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message='set field semantics target=field:field-1 desc=\"Approved semantic wording\" lock',\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_set_field_semantics\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"field_id\"] == \"field-1\"\n assert resp.intent[\"entities\"][\"description\"] == \"Approved semantic wording\"\n assert resp.intent[\"entities\"][\"lock_field\"] is True\n# #endregion test_dataset_review_scoped_command_routes_field_semantics_update\n# #endregion AssistantApiTests\n" + "body": "# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]\n# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.\n# @RELATION DEPENDS_ON -> [AssistantApi]\n# @INVARIANT Every test clears assistant in-memory state before execution.\nimport asyncio\nfrom datetime import UTC, datetime\nfrom unittest.mock import MagicMock\nimport uuid\n\nfrom src.api.routes import assistant as assistant_routes\nfrom src.models.assistant import AssistantMessageRecord\nfrom src.models.dataset_review import (\n ApprovalState,\n CandidateStatus,\n DatasetReviewSession,\n ExecutionMapping,\n ImportedFilter,\n MappingMethod,\n ReadinessState,\n RecommendedAction,\n SemanticCandidate,\n SemanticFieldEntry,\n SessionPhase,\n SessionStatus,\n)\nfrom src.schemas.auth import User\n\n\n# #region _run_async [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _run_async(coro):\n return asyncio.run(coro)\n# #endregion _run_async\n# #region _FakeTask [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests.\n# @INVARIANT status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.\nclass _FakeTask:\n def __init__(\n self,\n id,\n status=\"SUCCESS\",\n plugin_id=\"unknown\",\n params=None,\n result=None,\n user_id=None,\n ):\n self.id = id\n self.status = status\n self.plugin_id = plugin_id\n self.params = params or {}\n self.result = result or {}\n self.user_id = user_id\n self.started_at = datetime.now(UTC)\n self.finished_at = datetime.now(UTC)\n# #endregion _FakeTask\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF In-memory task manager stub that records created tasks for route-level assertions.\n# @INVARIANT create_task stores tasks retrievable by get_task/get_tasks without external side effects.\nclass _FakeTaskManager:\n def __init__(self):\n self.tasks = {}\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{uuid.uuid4().hex[:8]}\"\n task = _FakeTask(\n task_id,\n status=\"STARTED\",\n plugin_id=plugin_id,\n params=params,\n user_id=user_id,\n )\n self.tasks[task_id] = task\n return task\n def get_task(self, task_id):\n return self.tasks.get(task_id)\n def get_tasks(self, limit=20, offset=0):\n return sorted(self.tasks.values(), key=lambda t: t.id, reverse=True)[\n offset : offset + limit\n ]\n def get_all_tasks(self):\n return list(self.tasks.values())\n# #endregion _FakeTaskManager\n# #region _FakeConfigManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests.\n# @INVARIANT get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.\nclass _FakeConfigManager:\n class _Env:\n def __init__(self, id, name):\n self.id = id\n self.name = name\n def get_environments(self):\n return [self._Env(\"dev\", \"Development\"), self._Env(\"prod\", \"Production\")]\n def get_config(self):\n class _Settings:\n default_environment_id = \"dev\"\n llm = {}\n class _Config:\n settings = _Settings()\n environments = []\n return _Config()\n# #endregion _FakeConfigManager\n# #region _admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build admin principal with spec=User for assistant route authorization tests.\ndef _admin_user():\n user = MagicMock(spec=User)\n user.id = \"u-admin\"\n user.username = \"admin\"\n role = MagicMock()\n role.name = \"Admin\"\n user.roles = [role]\n return user\n# #endregion _admin_user\n# #region _limited_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build limited user principal with empty roles for assistant route denial tests.\ndef _limited_user():\n user = MagicMock(spec=User)\n user.id = \"u-limited\"\n user.username = \"limited\"\n user.roles = []\n return user\n# #endregion _limited_user\n# #region _FakeQuery [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths.\n# @INVARIANT filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.\nclass _FakeQuery:\n def __init__(self, items):\n self.items = items\n def outerjoin(self, *args, **kwargs):\n return self\n def options(self, *args, **kwargs):\n return self\n def filter(self, *args, **kwargs):\n # @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n def order_by(self, *args, **kwargs):\n return self\n def limit(self, n):\n self.items = self.items[:n]\n return self\n def offset(self, n):\n self.items = self.items[n:]\n return self\n def first(self):\n return self.items[0] if self.items else None\n def all(self):\n return self.items\n def count(self):\n return len(self.items)\n# #endregion _FakeQuery\n# #region _FakeDb [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Explicit in-memory DB session double limited to assistant message persistence paths.\n# @INVARIANT query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.\nclass _FakeDb:\n def __init__(self):\n self.added = []\n self.dataset_sessions = {}\n def query(self, model):\n if model == AssistantMessageRecord:\n return _FakeQuery([])\n if model == DatasetReviewSession:\n return _FakeQuery(list(self.dataset_sessions.values()))\n return _FakeQuery([])\n def add(self, obj):\n self.added.append(obj)\n def commit(self):\n pass\n def rollback(self):\n pass\n def merge(self, obj):\n return obj\n def refresh(self, obj):\n pass\n# #endregion _FakeDb\n# #region _clear_assistant_state [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\ndef _clear_assistant_state():\n assistant_routes.CONVERSATIONS.clear()\n assistant_routes.USER_ACTIVE_CONVERSATION.clear()\n assistant_routes.CONFIRMATIONS.clear()\n assistant_routes.ASSISTANT_AUDIT.clear()\n# #endregion _clear_assistant_state\n# #region _dataset_review_session [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n last_activity_at=datetime.now(UTC),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n# #endregion _dataset_review_session\n# #region _await_none [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Async helper returning None for planner fallback tests.\nasync def _await_none(*args, **kwargs):\n return None\n# #endregion _await_none\n# #region test_unknown_command_returns_needs_clarification [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Unknown command should return clarification state and unknown intent.\ndef test_unknown_command_returns_needs_clarification(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"some random gibberish\")\n # We mock LLM planner to return low confidence\n monkeypatch.setattr(assistant_routes, \"_plan_intent_with_llm\", lambda *a, **k: None)\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"needs_clarification\"\n assert \"уточните\" in resp.text.lower() or \"неоднозначна\" in resp.text.lower()\n# #endregion test_unknown_command_returns_needs_clarification\n# #region test_capabilities_question_returns_successful_help [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Capability query should return deterministic help response.\ndef test_capabilities_question_returns_successful_help(monkeypatch):\n _clear_assistant_state()\n req = assistant_routes.AssistantMessageRequest(message=\"что ты умеешь?\")\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert resp.state == \"success\"\n assert \"я могу сделать\" in resp.text.lower()\n# #endregion test_capabilities_question_returns_successful_help\n# #region test_assistant_message_request_accepts_dataset_review_session_binding [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Assistant request schema should accept active dataset review session binding for scoped orchestration.\ndef test_assistant_message_request_accepts_dataset_review_session_binding():\n request = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assert request.dataset_review_session_id == \"sess-1\"\n# #endregion test_assistant_message_request_accepts_dataset_review_session_binding\n# #region test_dataset_review_scoped_message_uses_masked_filter_context [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant context should mask imported-filter raw values before assistant-visible metadata is persisted.\ndef test_dataset_review_scoped_message_uses_masked_filter_context(monkeypatch):\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"show filters\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n async def _fake_dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n ):\n return str(intent[\"entities\"][\"summary\"]), None, []\n monkeypatch.setattr(\n assistant_routes,\n \"_dispatch_dataset_review_intent\",\n _fake_dispatch_dataset_review_intent,\n )\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"success\"\n persisted_assistant = [\n item for item in db.added if getattr(item, \"role\", None) == \"assistant\"\n ][-1]\n imported_filters = persisted_assistant.payload[\"dataset_review_context\"][\n \"imported_filters\"\n ]\n assert imported_filters[0][\"raw_value\"] == \"***@example.com\"\n assert imported_filters[0][\"raw_value_masked\"] is True\n# #endregion test_dataset_review_scoped_message_uses_masked_filter_context\n# #region test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route dataset-review mapping approvals into confirmation workflow with bound session metadata.\ndef test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message=\"approve mappings\",\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_approve_mappings\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"session_version\"] == 3\n assert resp.intent[\"entities\"][\"mapping_ids\"] == [\"map-1\"]\n# #endregion test_dataset_review_scoped_command_returns_confirmation_for_mapping_approval\n# #region test_dataset_review_scoped_command_routes_field_semantics_update [TYPE Function]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Session-scoped assistant commands should route semantic field updates through explicit confirmation metadata.\ndef test_dataset_review_scoped_command_routes_field_semantics_update():\n _clear_assistant_state()\n db = _FakeDb()\n db.dataset_sessions[\"sess-1\"] = _dataset_review_session()\n req = assistant_routes.AssistantMessageRequest(\n message='set field semantics target=field:field-1 desc=\"Approved semantic wording\" lock',\n dataset_review_session_id=\"sess-1\",\n )\n assistant_routes._plan_intent_with_llm = _await_none\n resp = _run_async(\n assistant_routes.send_message(\n req,\n current_user=_admin_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert resp.state == \"needs_confirmation\"\n assert resp.intent[\"operation\"] == \"dataset_review_set_field_semantics\"\n assert resp.intent[\"entities\"][\"dataset_review_session_id\"] == \"sess-1\"\n assert resp.intent[\"entities\"][\"field_id\"] == \"field-1\"\n assert resp.intent[\"entities\"][\"description\"] == \"Approved semantic wording\"\n assert resp.intent[\"entities\"][\"lock_field\"] is True\n# #endregion test_dataset_review_scoped_command_routes_field_semantics_update\n# #endregion AssistantApiTests\n" }, { "contract_id": "_dataset_review_session", @@ -394,7 +412,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _dataset_review_session [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n last_activity_at=datetime.utcnow(),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.utcnow(),\n updated_at=datetime.utcnow(),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n# #endregion _dataset_review_session\n" + "body": "# #region _dataset_review_session [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [AssistantApiTests]\n# @BRIEF Build minimal owned dataset-review session fixture for assistant scoped routing tests.\ndef _dataset_review_session():\n session = DatasetReviewSession(\n session_id=\"sess-1\",\n user_id=\"u-admin\",\n environment_id=\"env-1\",\n source_kind=\"superset_link\",\n source_input=\"http://superset.local/dashboard/10\",\n dataset_ref=\"public.sales\",\n dataset_id=42,\n version=3,\n readiness_state=ReadinessState.MAPPING_REVIEW_NEEDED,\n recommended_action=RecommendedAction.APPROVE_MAPPING,\n status=SessionStatus.ACTIVE,\n current_phase=SessionPhase.MAPPING_REVIEW,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n last_activity_at=datetime.now(UTC),\n )\n session.findings = []\n session.previews = []\n session.imported_filters = [\n ImportedFilter(\n filter_id=\"filter-1\",\n session_id=\"sess-1\",\n filter_name=\"email\",\n display_name=\"Email\",\n raw_value=\"john.doe@example.com\",\n raw_value_masked=False,\n normalized_value=\"john.doe@example.com\",\n source=\"manual\",\n confidence_state=\"confirmed\",\n requires_confirmation=False,\n recovery_status=\"recovered\",\n notes=None,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n )\n ]\n session.execution_mappings = [\n ExecutionMapping(\n mapping_id=\"map-1\",\n session_id=\"sess-1\",\n filter_id=\"filter-1\",\n variable_id=\"var-1\",\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=\"john.doe@example.com\",\n effective_value=\"john.doe@example.com\",\n transformation_note=None,\n warning_level=None,\n requires_explicit_approval=True,\n approval_state=ApprovalState.PENDING,\n approved_by_user_id=None,\n approved_at=None,\n created_at=datetime.now(UTC),\n updated_at=datetime.now(UTC),\n )\n ]\n session.semantic_fields = []\n session.semantic_fields = [\n SemanticFieldEntry(\n field_id=\"field-1\",\n session_id=\"sess-1\",\n field_name=\"customer_name\",\n field_kind=\"dimension\",\n verbose_name=\"Customer name\",\n description=\"Current semantic label\",\n display_format=\"text\",\n provenance=\"unresolved\",\n source_id=None,\n source_version=None,\n confidence_rank=None,\n is_locked=False,\n has_conflict=True,\n needs_review=True,\n last_changed_by=\"system\",\n )\n ]\n session.semantic_fields[0].candidates = [\n SemanticCandidate(\n candidate_id=\"cand-1\",\n field_id=\"field-1\",\n source_id=None,\n candidate_rank=1,\n match_type=\"exact\",\n confidence_score=0.99,\n proposed_verbose_name=\"Customer legal name\",\n proposed_description=\"Approved semantic wording\",\n proposed_display_format=\"text\",\n status=CandidateStatus.PROPOSED,\n )\n ]\n session.template_variables = []\n session.clarification_sessions = []\n session.run_contexts = []\n return session\n# #endregion _dataset_review_session\n" }, { "contract_id": "_await_none", @@ -590,7 +608,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]\n# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.\n# @LAYER API\n# @RELATION DEPENDS_ON -> AssistantApi\n# @INVARIANT Security-sensitive flows fail closed for unauthorized actors.\nimport asyncio\nfrom datetime import datetime, timedelta\nimport os\nimport pytest\nfrom types import SimpleNamespace\n\nfrom fastapi import HTTPException\n\n# Force isolated sqlite databases for test module before dependencies import.\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz.db\")\nos.environ.setdefault(\n \"TASKS_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_tasks.db\"\n)\nos.environ.setdefault(\n \"AUTH_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_auth.db\"\n)\nfrom src.api.routes import assistant as assistant_module\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\n\n# #region _run_async [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Execute async endpoint handler in synchronous test context.\n# @PRE coroutine is awaitable endpoint invocation.\n# @POST Returns coroutine result or raises propagated exception.\ndef _run_async(coroutine):\n return asyncio.run(coroutine)\n# #endregion _run_async\n# #region _FakeTask [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Lightweight task model used for assistant authz tests.\n# @PRE task_id is non-empty string.\n# @POST Returns task with provided id, status, and user_id accessible as attributes.\nclass _FakeTask:\n def __init__(self, task_id: str, status: str = \"RUNNING\", user_id: str = \"u-admin\"):\n self.id = task_id\n self.status = status\n self.user_id = user_id\n# #endregion _FakeTask\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF In-memory task manager double that records assistant-created tasks deterministically.\n# @INVARIANT Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.\nclass _FakeTaskManager:\n def __init__(self):\n self._created = []\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{len(self._created) + 1}\"\n task = _FakeTask(task_id=task_id, status=\"RUNNING\", user_id=user_id)\n self._created.append((plugin_id, params, user_id, task))\n return task\n def get_task(self, task_id):\n for _, _, _, task in self._created:\n if task.id == task_id:\n return task\n return None\n def get_tasks(self, limit=20, offset=0):\n return [x[3] for x in self._created][offset : offset + limit]\n def get_all_tasks(self):\n raise NotImplementedError(\n \"get_all_tasks not implemented in authz FakeTaskManager\"\n )\n# #endregion _FakeTaskManager\n# @CONTRACT: Partial ConfigManager stub for authz tests. Missing: get_config().\n# #region _FakeConfigManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Provide deterministic environment aliases required by intent parsing.\n# @PRE No external config or DB state is required.\n# @POST get_environments() returns two deterministic SimpleNamespace stubs with id/name.\n# @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.\nclass _FakeConfigManager:\n def get_environments(self):\n return [\n SimpleNamespace(id=\"dev\", name=\"Development\"),\n SimpleNamespace(id=\"prod\", name=\"Production\"),\n ]\n def get_config(self):\n raise NotImplementedError(\n \"get_config not implemented in authz fake — add if route under test requires it\"\n )\n# #endregion _FakeConfigManager\n# #region _admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build admin principal fixture.\n# @PRE Test requires privileged principal for risky operations.\n# @POST Returns admin-like user stub with Admin role.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin\", username=\"admin\", roles=[role])\n# #endregion _admin_user\n# #region _other_admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build second admin principal fixture for ownership tests.\n# @PRE Ownership mismatch scenario needs distinct authenticated actor.\n# @POST Returns alternate admin-like user stub.\ndef _other_admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin-2\", username=\"admin2\", roles=[role])\n# #endregion _other_admin_user\n# #region _limited_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build limited principal without required assistant execution privileges.\n# @PRE Permission denial scenario needs non-admin actor.\n# @POST Returns restricted user stub.\ndef _limited_user():\n role = SimpleNamespace(name=\"Operator\", permissions=[])\n return SimpleNamespace(id=\"u-limited\", username=\"limited\", roles=[role])\n# #endregion _limited_user\n# #region _FakeQuery [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Minimal chainable query object for fake DB interactions.\n# @INVARIANT filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.\nclass _FakeQuery:\n def __init__(self, rows):\n self._rows = list(rows)\n def filter(self, *args, **kwargs):\n # @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n def order_by(self, *args, **kwargs):\n return self\n def first(self):\n return self._rows[0] if self._rows else None\n def all(self):\n return list(self._rows)\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n def count(self):\n return len(self._rows)\n# #endregion _FakeQuery\n# #region _FakeDb [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.\n# @INVARIANT query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.\nclass _FakeDb:\n def __init__(self):\n self._messages = []\n self._confirmations = []\n self._audit = []\n def add(self, row):\n table = getattr(row, \"__tablename__\", \"\")\n if table == \"assistant_messages\":\n self._messages.append(row)\n elif table == \"assistant_confirmations\":\n self._confirmations.append(row)\n elif table == \"assistant_audit\":\n self._audit.append(row)\n def merge(self, row):\n if getattr(row, \"__tablename__\", \"\") != \"assistant_confirmations\":\n self.add(row)\n return row\n for i, existing in enumerate(self._confirmations):\n if getattr(existing, \"id\", None) == getattr(row, \"id\", None):\n self._confirmations[i] = row\n return row\n self._confirmations.append(row)\n return row\n def query(self, model):\n if model is AssistantMessageRecord:\n return _FakeQuery(self._messages)\n if model is AssistantConfirmationRecord:\n return _FakeQuery(self._confirmations)\n if model is AssistantAuditRecord:\n return _FakeQuery(self._audit)\n return _FakeQuery([])\n def commit(self):\n return None\n def rollback(self):\n return None\n# #endregion _FakeDb\n# #region _clear_assistant_state [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Reset assistant process-local state between test cases.\n# @PRE Assistant globals may contain state from prior tests.\n# @POST Assistant in-memory state dictionaries are cleared.\ndef _clear_assistant_state():\n assistant_module.CONVERSATIONS.clear()\n assistant_module.USER_ACTIVE_CONVERSATION.clear()\n assistant_module.CONFIRMATIONS.clear()\n assistant_module.ASSISTANT_AUDIT.clear()\n# #endregion _clear_assistant_state\n# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.\n# @PRE Confirmation token is created by first admin actor.\n# @POST Second actor receives 403 on confirm operation.\ndef test_confirmation_owner_mismatch_returns_403():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 18\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert create.state == \"needs_confirmation\"\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_other_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 403\n# #endregion test_confirmation_owner_mismatch_returns_403\n# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Expired confirmation token should be rejected and not create task.\n# @PRE Confirmation token exists and is manually expired before confirm request.\n# @POST Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.utcnow() - timedelta(minutes=1)\n )\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n# #endregion test_expired_confirmation_cannot_be_confirmed\n# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Limited user should receive denied state for privileged operation.\n# @PRE Restricted user attempts dangerous deploy command.\n# @POST Assistant returns denied state and does not execute operation.\ndef test_limited_user_cannot_launch_restricted_operation():\n _clear_assistant_state()\n response = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"задеплой дашборд 88 в production\"\n ),\n current_user=_limited_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert response.state == \"denied\"\n# #endregion test_limited_user_cannot_launch_restricted_operation\n# #endregion TestAssistantAuthz\n" + "body": "# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]\n# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.\n# @LAYER API\n# @RELATION DEPENDS_ON -> AssistantApi\n# @INVARIANT Security-sensitive flows fail closed for unauthorized actors.\nimport asyncio\nfrom datetime import UTC, datetime, timedelta\nimport os\nimport pytest\nfrom types import SimpleNamespace\n\nfrom fastapi import HTTPException\n\n# Force isolated sqlite databases for test module before dependencies import.\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz.db\")\nos.environ.setdefault(\n \"TASKS_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_tasks.db\"\n)\nos.environ.setdefault(\n \"AUTH_DATABASE_URL\", \"sqlite:////tmp/ss_tools_assistant_authz_auth.db\"\n)\nfrom src.api.routes import assistant as assistant_module\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\n\n# #region _run_async [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Execute async endpoint handler in synchronous test context.\n# @PRE coroutine is awaitable endpoint invocation.\n# @POST Returns coroutine result or raises propagated exception.\ndef _run_async(coroutine):\n return asyncio.run(coroutine)\n# #endregion _run_async\n# #region _FakeTask [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Lightweight task model used for assistant authz tests.\n# @PRE task_id is non-empty string.\n# @POST Returns task with provided id, status, and user_id accessible as attributes.\nclass _FakeTask:\n def __init__(self, task_id: str, status: str = \"RUNNING\", user_id: str = \"u-admin\"):\n self.id = task_id\n self.status = status\n self.user_id = user_id\n# #endregion _FakeTask\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF In-memory task manager double that records assistant-created tasks deterministically.\n# @INVARIANT Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.\nclass _FakeTaskManager:\n def __init__(self):\n self._created = []\n async def create_task(self, plugin_id, params, user_id=None):\n task_id = f\"task-{len(self._created) + 1}\"\n task = _FakeTask(task_id=task_id, status=\"RUNNING\", user_id=user_id)\n self._created.append((plugin_id, params, user_id, task))\n return task\n def get_task(self, task_id):\n for _, _, _, task in self._created:\n if task.id == task_id:\n return task\n return None\n def get_tasks(self, limit=20, offset=0):\n return [x[3] for x in self._created][offset : offset + limit]\n def get_all_tasks(self):\n raise NotImplementedError(\n \"get_all_tasks not implemented in authz FakeTaskManager\"\n )\n# #endregion _FakeTaskManager\n# @CONTRACT: Partial ConfigManager stub for authz tests. Missing: get_config().\n# #region _FakeConfigManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Provide deterministic environment aliases required by intent parsing.\n# @PRE No external config or DB state is required.\n# @POST get_environments() returns two deterministic SimpleNamespace stubs with id/name.\n# @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.\nclass _FakeConfigManager:\n def get_environments(self):\n return [\n SimpleNamespace(id=\"dev\", name=\"Development\"),\n SimpleNamespace(id=\"prod\", name=\"Production\"),\n ]\n def get_config(self):\n raise NotImplementedError(\n \"get_config not implemented in authz fake — add if route under test requires it\"\n )\n# #endregion _FakeConfigManager\n# #region _admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build admin principal fixture.\n# @PRE Test requires privileged principal for risky operations.\n# @POST Returns admin-like user stub with Admin role.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin\", username=\"admin\", roles=[role])\n# #endregion _admin_user\n# #region _other_admin_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build second admin principal fixture for ownership tests.\n# @PRE Ownership mismatch scenario needs distinct authenticated actor.\n# @POST Returns alternate admin-like user stub.\ndef _other_admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(id=\"u-admin-2\", username=\"admin2\", roles=[role])\n# #endregion _other_admin_user\n# #region _limited_user [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Build limited principal without required assistant execution privileges.\n# @PRE Permission denial scenario needs non-admin actor.\n# @POST Returns restricted user stub.\ndef _limited_user():\n role = SimpleNamespace(name=\"Operator\", permissions=[])\n return SimpleNamespace(id=\"u-limited\", username=\"limited\", roles=[role])\n# #endregion _limited_user\n# #region _FakeQuery [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Minimal chainable query object for fake DB interactions.\n# @INVARIANT filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.\nclass _FakeQuery:\n def __init__(self, rows):\n self._rows = list(rows)\n def filter(self, *args, **kwargs):\n # @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope\n return self\n def order_by(self, *args, **kwargs):\n return self\n def first(self):\n return self._rows[0] if self._rows else None\n def all(self):\n return list(self._rows)\n def limit(self, limit):\n self._rows = self._rows[:limit]\n return self\n def offset(self, offset):\n self._rows = self._rows[offset:]\n return self\n def count(self):\n return len(self._rows)\n# #endregion _FakeQuery\n# #region _FakeDb [TYPE Class] [C:2]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.\n# @INVARIANT query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.\nclass _FakeDb:\n def __init__(self):\n self._messages = []\n self._confirmations = []\n self._audit = []\n def add(self, row):\n table = getattr(row, \"__tablename__\", \"\")\n if table == \"assistant_messages\":\n self._messages.append(row)\n elif table == \"assistant_confirmations\":\n self._confirmations.append(row)\n elif table == \"assistant_audit\":\n self._audit.append(row)\n def merge(self, row):\n if getattr(row, \"__tablename__\", \"\") != \"assistant_confirmations\":\n self.add(row)\n return row\n for i, existing in enumerate(self._confirmations):\n if getattr(existing, \"id\", None) == getattr(row, \"id\", None):\n self._confirmations[i] = row\n return row\n self._confirmations.append(row)\n return row\n def query(self, model):\n if model is AssistantMessageRecord:\n return _FakeQuery(self._messages)\n if model is AssistantConfirmationRecord:\n return _FakeQuery(self._confirmations)\n if model is AssistantAuditRecord:\n return _FakeQuery(self._audit)\n return _FakeQuery([])\n def commit(self):\n return None\n def rollback(self):\n return None\n# #endregion _FakeDb\n# #region _clear_assistant_state [TYPE Function] [C:1]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Reset assistant process-local state between test cases.\n# @PRE Assistant globals may contain state from prior tests.\n# @POST Assistant in-memory state dictionaries are cleared.\ndef _clear_assistant_state():\n assistant_module.CONVERSATIONS.clear()\n assistant_module.USER_ACTIVE_CONVERSATION.clear()\n assistant_module.CONFIRMATIONS.clear()\n assistant_module.ASSISTANT_AUDIT.clear()\n# #endregion _clear_assistant_state\n# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.\n# @PRE Confirmation token is created by first admin actor.\n# @POST Second actor receives 403 on confirm operation.\ndef test_confirmation_owner_mismatch_returns_403():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 18\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert create.state == \"needs_confirmation\"\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_other_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 403\n# #endregion test_confirmation_owner_mismatch_returns_403\n# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Expired confirmation token should be rejected and not create task.\n# @PRE Confirmation token exists and is manually expired before confirm request.\n# @POST Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.now(UTC) - timedelta(minutes=1)\n )\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n# #endregion test_expired_confirmation_cannot_be_confirmed\n# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Limited user should receive denied state for privileged operation.\n# @PRE Restricted user attempts dangerous deploy command.\n# @POST Assistant returns denied state and does not execute operation.\ndef test_limited_user_cannot_launch_restricted_operation():\n _clear_assistant_state()\n response = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"задеплой дашборд 88 в production\"\n ),\n current_user=_limited_user(),\n task_manager=_FakeTaskManager(),\n config_manager=_FakeConfigManager(),\n db=_FakeDb(),\n )\n )\n assert response.state == \"denied\"\n# #endregion test_limited_user_cannot_launch_restricted_operation\n# #endregion TestAssistantAuthz\n" }, { "contract_id": "_run_async", @@ -831,7 +849,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Expired confirmation token should be rejected and not create task.\n# @PRE Confirmation token exists and is manually expired before confirm request.\n# @POST Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.utcnow() - timedelta(minutes=1)\n )\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n# #endregion test_expired_confirmation_cannot_be_confirmed\n" + "body": "# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]\n# @RELATION BINDS_TO -> [TestAssistantAuthz]\n# @BRIEF Expired confirmation token should be rejected and not create task.\n# @PRE Confirmation token exists and is manually expired before confirm request.\n# @POST Confirm endpoint raises 400 and no task is created.\ndef test_expired_confirmation_cannot_be_confirmed():\n _clear_assistant_state()\n task_manager = _FakeTaskManager()\n db = _FakeDb()\n create = _run_async(\n assistant_module.send_message(\n request=assistant_module.AssistantMessageRequest(\n message=\"запусти миграцию с dev на prod для дашборда 19\"\n ),\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assistant_module.CONFIRMATIONS[create.confirmation_id].expires_at = (\n datetime.now(UTC) - timedelta(minutes=1)\n )\n with pytest.raises(HTTPException) as exc:\n _run_async(\n assistant_module.confirm_operation(\n confirmation_id=create.confirmation_id,\n current_user=_admin_user(),\n task_manager=task_manager,\n config_manager=_FakeConfigManager(),\n db=db,\n )\n )\n assert exc.value.status_code == 400\n assert task_manager.get_tasks(limit=10, offset=0) == []\n# #endregion test_expired_confirmation_cannot_be_confirmed\n" }, { "contract_id": "test_limited_user_cannot_launch_restricted_operation", @@ -4155,7 +4173,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestReportsApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, contract, pagination, filtering]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior.\n# @LAYER Domain\n# @INVARIANT API response contract contains {items,total,page,page_size,has_next,applied_filters}.\nfrom datetime import UTC, datetime, timedelta\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsApi]\n# @BRIEF Minimal task-manager double exposing only get_all_tasks used by reports route tests.\n# @INVARIANT Returns pre-seeded tasks without mutation or side effects.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Build deterministic admin principal accepted by reports authorization guard.\ndef _admin_user():\n admin_role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[admin_role])\n# #endregion _admin_user\n# #region _make_task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Build Task fixture with controlled timestamps/status for reports list/detail normalization.\ndef _make_task(\n task_id: str,\n plugin_id: str,\n status: TaskStatus,\n started_at: datetime,\n finished_at: datetime = None,\n result=None,\n):\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=started_at,\n finished_at=finished_at,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _make_task\n# #region test_get_reports_default_pagination_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_default_pagination_contract\n# #region test_get_reports_filter_and_pagination [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_filter_and_pagination\n# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.utcnow()\n aware_now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes\n# #region test_get_reports_invalid_filter_returns_400 [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_invalid_filter_returns_400\n# #endregion TestReportsApi\n" + "body": "# #region TestReportsApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, contract, pagination, filtering]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior.\n# @LAYER Domain\n# @INVARIANT API response contract contains {items,total,page,page_size,has_next,applied_filters}.\nfrom datetime import UTC, datetime, timedelta\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsApi]\n# @BRIEF Minimal task-manager double exposing only get_all_tasks used by reports route tests.\n# @INVARIANT Returns pre-seeded tasks without mutation or side effects.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Build deterministic admin principal accepted by reports authorization guard.\ndef _admin_user():\n admin_role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[admin_role])\n# #endregion _admin_user\n# #region _make_task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Build Task fixture with controlled timestamps/status for reports list/detail normalization.\ndef _make_task(\n task_id: str,\n plugin_id: str,\n status: TaskStatus,\n started_at: datetime,\n finished_at: datetime = None,\n result=None,\n):\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=started_at,\n finished_at=finished_at,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _make_task\n# #region test_get_reports_default_pagination_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_default_pagination_contract\n# #region test_get_reports_filter_and_pagination [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_filter_and_pagination\n# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.now(UTC)\n aware_now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes\n# #region test_get_reports_invalid_filter_returns_400 [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_invalid_filter_returns_400\n# #endregion TestReportsApi\n" }, { "contract_id": "_make_task", @@ -4203,7 +4221,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_get_reports_default_pagination_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_default_pagination_contract\n" + "body": "# #region test_get_reports_default_pagination_contract [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint default pagination and contract keys for mixed task statuses.\ndef test_get_reports_default_pagination_contract():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n _make_task(\n \"t-2\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=8),\n now - timedelta(minutes=7),\n ),\n _make_task(\n \"t-3\",\n \"llm_dashboard_validation\",\n TaskStatus.RUNNING,\n now - timedelta(minutes=6),\n None,\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n data = response.json()\n assert set(\n [\"items\", \"total\", \"page\", \"page_size\", \"has_next\", \"applied_filters\"]\n ).issubset(data.keys())\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n assert data[\"total\"] == 3\n assert isinstance(data[\"items\"], list)\n assert data[\"applied_filters\"][\"sort_by\"] == \"updated_at\"\n assert data[\"applied_filters\"][\"sort_order\"] == \"desc\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_default_pagination_contract\n" }, { "contract_id": "test_get_reports_filter_and_pagination", @@ -4227,7 +4245,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_get_reports_filter_and_pagination [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_filter_and_pagination\n" + "body": "# #region test_get_reports_filter_and_pagination [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint applies task-type/status filters and pagination boundaries.\ndef test_get_reports_filter_and_pagination():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=30),\n now - timedelta(minutes=29),\n ),\n _make_task(\n \"t-2\",\n \"superset-backup\",\n TaskStatus.FAILED,\n now - timedelta(minutes=20),\n now - timedelta(minutes=19),\n ),\n _make_task(\n \"t-3\",\n \"superset-migration\",\n TaskStatus.FAILED,\n now - timedelta(minutes=10),\n now - timedelta(minutes=9),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\n \"/api/reports?task_types=backup&statuses=failed&page=1&page_size=1\"\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 1\n assert data[\"has_next\"] is False\n assert len(data[\"items\"]) == 1\n assert data[\"items\"][0][\"task_type\"] == \"backup\"\n assert data[\"items\"][0][\"status\"] == \"failed\"\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_filter_and_pagination\n" }, { "contract_id": "test_get_reports_handles_mixed_naive_and_aware_datetimes", @@ -4251,7 +4269,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.utcnow()\n aware_now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes\n" + "body": "# #region test_get_reports_handles_mixed_naive_and_aware_datetimes [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports sorting remains stable when task timestamps mix naive and timezone-aware datetimes.\ndef test_get_reports_handles_mixed_naive_and_aware_datetimes():\n naive_now = datetime.now(UTC)\n aware_now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-naive\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n naive_now - timedelta(minutes=5),\n naive_now - timedelta(minutes=4),\n ),\n _make_task(\n \"t-aware\",\n \"superset-migration\",\n TaskStatus.FAILED,\n aware_now - timedelta(minutes=3),\n aware_now - timedelta(minutes=2),\n ),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?sort_by=updated_at&sort_order=desc\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"total\"] == 2\n assert len(data[\"items\"]) == 2\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_handles_mixed_naive_and_aware_datetimes\n" }, { "contract_id": "test_get_reports_invalid_filter_returns_400", @@ -4275,7 +4293,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_get_reports_invalid_filter_returns_400 [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.utcnow()\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_invalid_filter_returns_400\n" + "body": "# #region test_get_reports_invalid_filter_returns_400 [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsApi\n# @BRIEF Validate reports list endpoint rejects unsupported task type filters with HTTP 400.\ndef test_get_reports_invalid_filter_returns_400():\n now = datetime.now(UTC)\n tasks = [\n _make_task(\n \"t-1\",\n \"superset-backup\",\n TaskStatus.SUCCESS,\n now - timedelta(minutes=5),\n now - timedelta(minutes=4),\n )\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports?task_types=bad_type\")\n assert response.status_code == 400\n body = response.json()\n assert \"detail\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_reports_invalid_filter_returns_400\n" }, { "contract_id": "TestReportsDetailApi", @@ -4303,7 +4321,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestReportsDetailApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, detail, diagnostics]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior.\n# @LAYER Domain\n# @INVARIANT Detail endpoint tests must keep deterministic assertions for success and not-found contracts.\nfrom datetime import datetime, timedelta\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsDetailApi]\n# @BRIEF Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test.\n# @INVARIANT get_all_tasks returns exactly seeded tasks list.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Provide admin principal fixture accepted by reports detail authorization policy.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n# #endregion _admin_user\n# #region _make_task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Build deterministic Task payload for reports detail endpoint contract assertions.\ndef _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now - timedelta(minutes=2),\n finished_at=now - timedelta(minutes=1)\n if status != TaskStatus.RUNNING\n else None,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} result\"},\n )\n# #endregion _make_task\n# #region test_get_report_detail_success [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns report body with diagnostics and next actions for existing task.\ndef test_get_report_detail_success():\n task = _make_task(\n \"detail-1\",\n \"superset-migration\",\n TaskStatus.FAILED,\n result={\n \"error\": {\n \"message\": \"Step failed\",\n \"next_actions\": [\"Check mapping\", \"Retry\"],\n }\n },\n )\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/detail-1\")\n assert response.status_code == 200\n data = response.json()\n assert \"report\" in data\n assert data[\"report\"][\"report_id\"] == \"detail-1\"\n assert \"diagnostics\" in data\n assert \"next_actions\" in data\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_success\n# #region test_get_report_detail_not_found [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns 404 when requested report identifier is absent.\ndef test_get_report_detail_not_found():\n task = _make_task(\"detail-2\", \"superset-backup\", TaskStatus.SUCCESS)\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/unknown-id\")\n assert response.status_code == 404\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_not_found\n# #endregion TestReportsDetailApi\n" + "body": "# #region TestReportsDetailApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, detail, diagnostics]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior.\n# @LAYER Domain\n# @INVARIANT Detail endpoint tests must keep deterministic assertions for success and not-found contracts.\nfrom datetime import UTC, datetime, timedelta\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# @DEBT: Divergent _FakeTaskManager definition. Canonical version should be in conftest.py. Authz variant is missing get_all_tasks().\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsDetailApi]\n# @BRIEF Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test.\n# @INVARIANT get_all_tasks returns exactly seeded tasks list.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Provide admin principal fixture accepted by reports detail authorization policy.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n# #endregion _admin_user\n# #region _make_task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Build deterministic Task payload for reports detail endpoint contract assertions.\ndef _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None):\n now = datetime.now(UTC)\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now - timedelta(minutes=2),\n finished_at=now - timedelta(minutes=1)\n if status != TaskStatus.RUNNING\n else None,\n params={\"environment_id\": \"env-1\"},\n result=result or {\"summary\": f\"{plugin_id} result\"},\n )\n# #endregion _make_task\n# #region test_get_report_detail_success [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns report body with diagnostics and next actions for existing task.\ndef test_get_report_detail_success():\n task = _make_task(\n \"detail-1\",\n \"superset-migration\",\n TaskStatus.FAILED,\n result={\n \"error\": {\n \"message\": \"Step failed\",\n \"next_actions\": [\"Check mapping\", \"Retry\"],\n }\n },\n )\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/detail-1\")\n assert response.status_code == 200\n data = response.json()\n assert \"report\" in data\n assert data[\"report\"][\"report_id\"] == \"detail-1\"\n assert \"diagnostics\" in data\n assert \"next_actions\" in data\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_success\n# #region test_get_report_detail_not_found [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsDetailApi\n# @BRIEF Validate report detail endpoint returns 404 when requested report identifier is absent.\ndef test_get_report_detail_not_found():\n task = _make_task(\"detail-2\", \"superset-backup\", TaskStatus.SUCCESS)\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/unknown-id\")\n assert response.status_code == 404\n finally:\n app.dependency_overrides.clear()\n# #endregion test_get_report_detail_not_found\n# #endregion TestReportsDetailApi\n" }, { "contract_id": "test_get_report_detail_success", @@ -4378,7 +4396,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestReportsOpenapiConformance [TYPE Module] [C:3] [SEMANTICS tests, reports, openapi, conformance]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields.\n# @LAYER Domain\n# @INVARIANT List and detail payloads include required contract keys.\nfrom datetime import datetime\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsOpenapiConformance]\n# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks.\n# @INVARIANT get_all_tasks returns seeded tasks unchanged.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Provide admin principal fixture required by reports routes in conformance tests.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n# #endregion _admin_user\n# #region _task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _task\n# #region test_reports_list_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports list endpoint includes all required OpenAPI top-level keys.\ndef test_reports_list_openapi_required_keys():\n tasks = [\n _task(\"r-1\", \"superset-backup\", TaskStatus.SUCCESS),\n _task(\"r-2\", \"superset-migration\", TaskStatus.FAILED),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n body = response.json()\n required = {\n \"items\",\n \"total\",\n \"page\",\n \"page_size\",\n \"has_next\",\n \"applied_filters\",\n }\n assert required.issubset(body.keys())\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_list_openapi_required_keys\n# #region test_reports_detail_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports detail endpoint returns payload containing the report object key.\ndef test_reports_detail_openapi_required_keys():\n tasks = [_task(\"r-3\", \"llm_dashboard_validation\", TaskStatus.SUCCESS)]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/r-3\")\n assert response.status_code == 200\n body = response.json()\n assert \"report\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_detail_openapi_required_keys\n# #endregion TestReportsOpenapiConformance\n" + "body": "# #region TestReportsOpenapiConformance [TYPE Module] [C:3] [SEMANTICS tests, reports, openapi, conformance]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields.\n# @LAYER Domain\n# @INVARIANT List and detail payloads include required contract keys.\nfrom datetime import UTC, datetime\nfrom types import SimpleNamespace\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.core.task_manager.models import Task, TaskStatus\nfrom src.dependencies import get_current_user, get_task_manager\n\n\n# #region _FakeTaskManager [TYPE Class] [C:1]\n# @RELATION BINDS_TO -> [TestReportsOpenapiConformance]\n# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks.\n# @INVARIANT get_all_tasks returns seeded tasks unchanged.\nclass _FakeTaskManager:\n def __init__(self, tasks):\n self._tasks = tasks\n def get_all_tasks(self):\n return self._tasks\n# #endregion _FakeTaskManager\n# #region _admin_user [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Provide admin principal fixture required by reports routes in conformance tests.\ndef _admin_user():\n role = SimpleNamespace(name=\"Admin\", permissions=[])\n return SimpleNamespace(username=\"test-admin\", roles=[role])\n# #endregion _admin_user\n# #region _task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.now(UTC)\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _task\n# #region test_reports_list_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports list endpoint includes all required OpenAPI top-level keys.\ndef test_reports_list_openapi_required_keys():\n tasks = [\n _task(\"r-1\", \"superset-backup\", TaskStatus.SUCCESS),\n _task(\"r-2\", \"superset-migration\", TaskStatus.FAILED),\n ]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports\")\n assert response.status_code == 200\n body = response.json()\n required = {\n \"items\",\n \"total\",\n \"page\",\n \"page_size\",\n \"has_next\",\n \"applied_filters\",\n }\n assert required.issubset(body.keys())\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_list_openapi_required_keys\n# #region test_reports_detail_openapi_required_keys [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Verify reports detail endpoint returns payload containing the report object key.\ndef test_reports_detail_openapi_required_keys():\n tasks = [_task(\"r-3\", \"llm_dashboard_validation\", TaskStatus.SUCCESS)]\n app.dependency_overrides[get_current_user] = lambda: _admin_user()\n app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)\n try:\n client = TestClient(app)\n response = client.get(\"/api/reports/r-3\")\n assert response.status_code == 200\n body = response.json()\n assert \"report\" in body\n finally:\n app.dependency_overrides.clear()\n# #endregion test_reports_detail_openapi_required_keys\n# #endregion TestReportsOpenapiConformance\n" }, { "contract_id": "_FakeTaskManager", @@ -4452,7 +4470,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.utcnow()\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _task\n" + "body": "# #region _task [TYPE Function]\n# @RELATION BINDS_TO -> TestReportsOpenapiConformance\n# @BRIEF Construct deterministic task fixture consumed by reports list/detail payload assertions.\ndef _task(task_id: str, plugin_id: str, status: TaskStatus):\n now = datetime.now(UTC)\n return Task(\n id=task_id,\n plugin_id=plugin_id,\n status=status,\n started_at=now,\n finished_at=now if status != TaskStatus.RUNNING else None,\n params={\"environment_id\": \"env-1\"},\n result={\"summary\": f\"{plugin_id} {status.value.lower()}\"},\n )\n# #endregion _task\n" }, { "contract_id": "test_reports_list_openapi_required_keys", @@ -4981,7 +4999,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AdminApiKeyRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, admin, api_key, crud]\n# @BRIEF Admin API endpoints for API key management — list, generate (one-time reveal), and revoke.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [APIKeyUtilities]\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_admin_settings_WRITE]\n# @INVARIANT GET /api/admin/api-keys NEVER returns key_hash or raw_key.\n# @INVARIANT POST /api/admin/api-keys returns raw_key ONCE — never stored, never retrievable again.\n# @INVARIANT DELETE /api/admin/api-keys/{id} soft-deletes (active=False), preserves row for audit.\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, Field\nfrom sqlalchemy.orm import Session\n\nfrom ...core.auth.api_key import generate_api_key\nfrom ...core.database import get_db\nfrom ...dependencies import has_permission\nfrom ...models.api_key import APIKey\n\n# #region router [TYPE Variable]\n# @BRIEF APIRouter for admin API key management routes.\nrouter = APIRouter(prefix=\"/api/admin/api-keys\", tags=[\"admin\", \"api-keys\"])\n# #endregion router\n\n\n# ── Pydantic schemas ──────────────────────────────────────────\n\n# #region ApiKeyCreateRequest [C:1] [TYPE Class]\nclass ApiKeyCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n environment_id: str | None = None\n permissions: list[str] = Field(..., min_length=1)\n expires_at: datetime | None = None\n# #endregion ApiKeyCreateRequest\n\n\n# #region ApiKeyCreateResponse [C:1] [TYPE Class]\nclass ApiKeyCreateResponse(BaseModel):\n id: str\n raw_key: str\n prefix: str\n name: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n# #endregion ApiKeyCreateResponse\n\n\n# #region ApiKeyListItem [C:1] [TYPE Class]\nclass ApiKeyListItem(BaseModel):\n id: str\n name: str\n prefix: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n last_used_at: datetime | None\n\n model_config = {\"from_attributes\": True}\n# #endregion ApiKeyListItem\n\n\n# #region ApiKeyRevokeResponse [C:1] [TYPE Class]\nclass ApiKeyRevokeResponse(BaseModel):\n id: str\n status: str\n# #endregion ApiKeyRevokeResponse\n\n\n# ── Routes ────────────────────────────────────────────────────\n\n# #region list_api_keys [C:2] [TYPE Function]\n# @BRIEF List all API keys — NEVER returns key_hash or raw_key.\n# @PRE Requires admin:settings WRITE permission.\n# @POST Returns list of ApiKeyListItem without sensitive fields.\n@router.get(\"/\", response_model=list[ApiKeyListItem])\nasync def list_api_keys(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()\n return [\n ApiKeyListItem(\n id=k.id,\n name=k.name,\n prefix=k.prefix,\n environment_id=k.environment_id,\n permissions=list(k.permissions or []),\n active=k.active,\n created_at=k.created_at,\n expires_at=k.expires_at,\n last_used_at=k.last_used_at,\n )\n for k in keys\n ]\n# #endregion list_api_keys\n\n\n# #region create_api_key [C:3] [TYPE Function]\n# @BRIEF Generate a new API key — returns raw key ONCE, never stored or retrievable again.\n# @PRE Requires admin:settings WRITE permission. name is required, at least one permission.\n# @POST Creates APIKey row with SHA-256 hash. Returns raw key in response.\n# @SIDE_EFFECT Generates cryptographically random key, stores hash in DB.\n# @RELATION DEPENDS_ON -> [generate_api_key]\n@router.post(\"/\", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)\nasync def create_api_key(\n request: ApiKeyCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n # Validate\n if not request.name.strip():\n raise HTTPException(status_code=400, detail=\"Name is required\")\n if not request.permissions:\n raise HTTPException(status_code=400, detail=\"At least one permission is required\")\n\n # Generate key\n raw_key, prefix, key_hash = generate_api_key()\n\n # Store hash only\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=request.name.strip(),\n environment_id=request.environment_id,\n permissions=request.permissions,\n active=True,\n expires_at=request.expires_at,\n )\n db.add(api_key)\n db.commit()\n db.refresh(api_key)\n\n return ApiKeyCreateResponse(\n id=api_key.id,\n raw_key=raw_key,\n prefix=api_key.prefix,\n name=api_key.name,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n active=api_key.active,\n created_at=api_key.created_at,\n expires_at=api_key.expires_at,\n )\n# #endregion create_api_key\n\n\n# #region revoke_api_key [C:2] [TYPE Function]\n# @BRIEF Revoke an API key by setting active=False. Preserves row for audit.\n# @PRE Requires admin:settings WRITE permission.\n# @POST Sets active=False on the key. Returns 404 if already revoked or not found.\n@router.delete(\"/{key_id}\", response_model=ApiKeyRevokeResponse)\nasync def revoke_api_key(\n key_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n api_key = db.query(APIKey).filter(APIKey.id == key_id).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"API key not found\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"API key is already revoked\",\n )\n\n api_key.active = False\n db.commit()\n\n return ApiKeyRevokeResponse(\n id=api_key.id,\n status=\"revoked\",\n )\n# #endregion revoke_api_key\n\n# #endregion AdminApiKeyRoutes\n" + "body": "# #region AdminApiKeyRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, admin, api_key, crud]\n# @BRIEF Admin API endpoints for API key management — list, generate (one-time reveal), and revoke.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [APIKeyUtilities]\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_admin_settings_WRITE]\n# @INVARIANT GET /api/admin/api-keys NEVER returns key_hash or raw_key.\n# @INVARIANT POST /api/admin/api-keys returns raw_key ONCE — never stored, never retrievable again.\n# @INVARIANT DELETE /api/admin/api-keys/{id} soft-deletes (active=False), preserves row for audit.\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, ConfigDict, Field\nfrom sqlalchemy.orm import Session\n\nfrom ...core.auth.api_key import generate_api_key\nfrom ...core.database import get_db\nfrom ...dependencies import has_permission\nfrom ...models.api_key import APIKey\n\n# #region router [TYPE Variable]\n# @BRIEF APIRouter for admin API key management routes.\nrouter = APIRouter(prefix=\"/api/admin/api-keys\", tags=[\"admin\", \"api-keys\"])\n# #endregion router\n\n\n# ── Pydantic schemas ──────────────────────────────────────────\n\n# #region ApiKeyCreateRequest [C:1] [TYPE Class]\nclass ApiKeyCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n environment_id: str | None = None\n permissions: list[str] = Field(..., min_length=1)\n expires_at: datetime | None = None\n# #endregion ApiKeyCreateRequest\n\n\n# #region ApiKeyCreateResponse [C:1] [TYPE Class]\nclass ApiKeyCreateResponse(BaseModel):\n id: str\n raw_key: str\n prefix: str\n name: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n# #endregion ApiKeyCreateResponse\n\n\n# #region ApiKeyListItem [C:1] [TYPE Class]\nclass ApiKeyListItem(BaseModel):\n id: str\n name: str\n prefix: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n last_used_at: datetime | None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ApiKeyListItem\n\n\n# #region ApiKeyRevokeResponse [C:1] [TYPE Class]\nclass ApiKeyRevokeResponse(BaseModel):\n id: str\n status: str\n# #endregion ApiKeyRevokeResponse\n\n\n# ── Routes ────────────────────────────────────────────────────\n\n# #region list_api_keys [C:2] [TYPE Function]\n# @BRIEF List all API keys — NEVER returns key_hash or raw_key.\n# @PRE Requires admin:settings WRITE permission.\n# @POST Returns list of ApiKeyListItem without sensitive fields.\n@router.get(\"/\", response_model=list[ApiKeyListItem])\nasync def list_api_keys(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()\n return [\n ApiKeyListItem(\n id=k.id,\n name=k.name,\n prefix=k.prefix,\n environment_id=k.environment_id,\n permissions=list(k.permissions or []),\n active=k.active,\n created_at=k.created_at,\n expires_at=k.expires_at,\n last_used_at=k.last_used_at,\n )\n for k in keys\n ]\n# #endregion list_api_keys\n\n\n# #region create_api_key [C:3] [TYPE Function]\n# @BRIEF Generate a new API key — returns raw key ONCE, never stored or retrievable again.\n# @PRE Requires admin:settings WRITE permission. name is required, at least one permission.\n# @POST Creates APIKey row with SHA-256 hash. Returns raw key in response.\n# @SIDE_EFFECT Generates cryptographically random key, stores hash in DB.\n# @RELATION DEPENDS_ON -> [generate_api_key]\n@router.post(\"/\", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)\nasync def create_api_key(\n request: ApiKeyCreateRequest,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n # Validate\n if not request.name.strip():\n raise HTTPException(status_code=400, detail=\"Name is required\")\n if not request.permissions:\n raise HTTPException(status_code=400, detail=\"At least one permission is required\")\n\n # Generate key\n raw_key, prefix, key_hash = generate_api_key()\n\n # Store hash only\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=request.name.strip(),\n environment_id=request.environment_id,\n permissions=request.permissions,\n active=True,\n expires_at=request.expires_at,\n )\n db.add(api_key)\n db.commit()\n db.refresh(api_key)\n\n return ApiKeyCreateResponse(\n id=api_key.id,\n raw_key=raw_key,\n prefix=api_key.prefix,\n name=api_key.name,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n active=api_key.active,\n created_at=api_key.created_at,\n expires_at=api_key.expires_at,\n )\n# #endregion create_api_key\n\n\n# #region revoke_api_key [C:2] [TYPE Function]\n# @BRIEF Revoke an API key by setting active=False. Preserves row for audit.\n# @PRE Requires admin:settings WRITE permission.\n# @POST Sets active=False on the key. Returns 404 if already revoked or not found.\n@router.delete(\"/{key_id}\", response_model=ApiKeyRevokeResponse)\nasync def revoke_api_key(\n key_id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n api_key = db.query(APIKey).filter(APIKey.id == key_id).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"API key not found\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"API key is already revoked\",\n )\n\n api_key.active = False\n db.commit()\n\n return ApiKeyRevokeResponse(\n id=api_key.id,\n status=\"revoked\",\n )\n# #endregion revoke_api_key\n\n# #endregion AdminApiKeyRoutes\n" }, { "contract_id": "ApiKeyCreateRequest", @@ -5032,7 +5050,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ApiKeyListItem [C:1] [TYPE Class]\nclass ApiKeyListItem(BaseModel):\n id: str\n name: str\n prefix: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n last_used_at: datetime | None\n\n model_config = {\"from_attributes\": True}\n# #endregion ApiKeyListItem\n" + "body": "# #region ApiKeyListItem [C:1] [TYPE Class]\nclass ApiKeyListItem(BaseModel):\n id: str\n name: str\n prefix: str\n environment_id: str | None\n permissions: list[str]\n active: bool\n created_at: datetime\n expires_at: datetime | None\n last_used_at: datetime | None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ApiKeyListItem\n" }, { "contract_id": "ApiKeyRevokeResponse", @@ -5156,7 +5174,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantAdminRoutes [C:5] [TYPE Module] [SEMANTICS assistant, admin, route, audit, conversation]\n# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantRoutes]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @INVARIANT Audit endpoint requires tasks:READ permission.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy import desc\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_current_user, has_permission\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantMessageRecord,\n)\nfrom src.schemas.auth import User\n\nfrom ._history import (\n _cleanup_history_ttl,\n _coerce_query_bool,\n _is_conversation_archived,\n _resolve_or_create_conversation,\n)\nfrom ._routes import router\nfrom ._schemas import (\n ASSISTANT_AUDIT,\n CONVERSATIONS,\n)\n\n\n# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE Authenticated user context and valid pagination params.\n# @POST Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n\n\n# #region delete_conversation [C:2] [TYPE Function]\n# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE conversation_id belongs to current_user.\n# @POST Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# #endregion delete_conversation\n\n\n@router.get(\"/history\")\n# #region get_history [TYPE Function]\n# @BRIEF Retrieve paginated assistant conversation history for current user.\n# @PRE Authenticated user is available and page params are valid.\n# @POST Returns persistent messages and mirrored in-memory snapshot for diagnostics.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: str | None = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# #endregion get_history\n\n\n@router.get(\"/audit\")\n# #region get_assistant_audit [TYPE Function]\n# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE User has tasks:READ permission.\n# @POST Audit payload is returned in reverse chronological order from DB.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# #endregion get_assistant_audit\n\n\n# #endregion AssistantAdminRoutes\n" + "body": "# #region AssistantAdminRoutes [C:5] [TYPE Module] [SEMANTICS assistant, admin, route, audit, conversation]\n# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantRoutes]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @INVARIANT Audit endpoint requires tasks:READ permission.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import Depends, HTTPException, Query\nfrom sqlalchemy import desc\nfrom sqlalchemy.orm import Session\n\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_current_user, has_permission\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantMessageRecord,\n)\nfrom src.schemas.auth import User\n\nfrom ._history import (\n _cleanup_history_ttl,\n _coerce_query_bool,\n _is_conversation_archived,\n _resolve_or_create_conversation,\n)\nfrom ._routes import router\nfrom ._schemas import (\n ASSISTANT_AUDIT,\n CONVERSATIONS,\n)\n\n\n# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE Authenticated user context and valid pagination params.\n# @POST Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.now()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n\n\n# #region delete_conversation [C:2] [TYPE Function]\n# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.\n# @PRE conversation_id belongs to current_user.\n# @POST Conversation records are removed from DB and CONVERSATIONS cache.\n@router.delete(\"/conversations/{conversation_id}\")\nasync def delete_conversation(\n conversation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations.delete\"):\n user_id = current_user.id\n\n # 1. Remove from in-memory cache\n key = (user_id, conversation_id)\n if key in CONVERSATIONS:\n del CONVERSATIONS[key]\n\n # 2. Delete from database\n deleted_count = (\n db.query(AssistantMessageRecord)\n .filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conversation_id,\n )\n .delete()\n )\n\n db.commit()\n\n if deleted_count == 0:\n raise HTTPException(\n status_code=404, detail=\"Conversation not found or already deleted\"\n )\n\n return {\n \"status\": \"success\",\n \"deleted\": deleted_count,\n \"conversation_id\": conversation_id,\n }\n\n\n# #endregion delete_conversation\n\n\n@router.get(\"/history\")\n# #region get_history [TYPE Function]\n# @BRIEF Retrieve paginated assistant conversation history for current user.\n# @PRE Authenticated user is available and page params are valid.\n# @POST Returns persistent messages and mirrored in-memory snapshot for diagnostics.\nasync def get_history(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n conversation_id: str | None = Query(None),\n from_latest: bool = Query(False),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.history\"):\n user_id = current_user.id\n _cleanup_history_ttl(db, user_id)\n conv_id = _resolve_or_create_conversation(user_id, conversation_id, db)\n\n base_query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.conversation_id == conv_id,\n )\n total = base_query.count()\n start = (page - 1) * page_size\n if from_latest:\n rows = (\n base_query.order_by(desc(AssistantMessageRecord.created_at))\n .offset(start)\n .limit(page_size)\n .all()\n )\n rows = list(reversed(rows))\n else:\n rows = (\n base_query.order_by(AssistantMessageRecord.created_at.asc())\n .offset(start)\n .limit(page_size)\n .all()\n )\n\n persistent_items = [\n {\n \"message_id\": row.id,\n \"conversation_id\": row.conversation_id,\n \"role\": row.role,\n \"text\": row.text,\n \"state\": row.state,\n \"task_id\": row.task_id,\n \"confirmation_id\": row.confirmation_id,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n \"metadata\": row.payload,\n }\n for row in rows\n ]\n\n memory_items = CONVERSATIONS.get((user_id, conv_id), [])\n return {\n \"items\": persistent_items,\n \"memory_items\": memory_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"from_latest\": from_latest,\n \"conversation_id\": conv_id,\n }\n\n\n# #endregion get_history\n\n\n@router.get(\"/audit\")\n# #region get_assistant_audit [TYPE Function]\n# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.\n# @PRE User has tasks:READ permission.\n# @POST Audit payload is returned in reverse chronological order from DB.\nasync def get_assistant_audit(\n limit: int = Query(50, ge=1, le=500),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"tasks\", \"READ\")),\n):\n with belief_scope(\"assistant.audit\"):\n memory_rows = ASSISTANT_AUDIT.get(current_user.id, [])\n db_rows = (\n db.query(AssistantAuditRecord)\n .filter(AssistantAuditRecord.user_id == current_user.id)\n .order_by(AssistantAuditRecord.created_at.desc())\n .limit(limit)\n .all()\n )\n persistent = [\n {\n \"id\": row.id,\n \"user_id\": row.user_id,\n \"conversation_id\": row.conversation_id,\n \"decision\": row.decision,\n \"task_id\": row.task_id,\n \"message\": row.message,\n \"payload\": row.payload,\n \"created_at\": row.created_at.isoformat() if row.created_at else None,\n }\n for row in db_rows\n ]\n return {\n \"items\": persistent,\n \"memory_items\": memory_rows[-limit:],\n \"total\": len(persistent),\n \"memory_total\": len(memory_rows),\n }\n\n\n# #endregion get_assistant_audit\n\n\n# #endregion AssistantAdminRoutes\n" }, { "contract_id": "list_conversations", @@ -5176,7 +5194,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE Authenticated user context and valid pagination params.\n# @POST Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.utcnow()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n" + "body": "# #region list_conversations [C:2] [TYPE Function]\n# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.\n# @PRE Authenticated user context and valid pagination params.\n# @POST Conversations are grouped by conversation_id sorted by latest activity descending.\n@router.get(\"/conversations\")\nasync def list_conversations(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n include_archived: bool = Query(False),\n archived_only: bool = Query(False),\n search: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.conversations\"):\n user_id = current_user.id\n include_archived = _coerce_query_bool(include_archived)\n archived_only = _coerce_query_bool(archived_only)\n _cleanup_history_ttl(db, user_id)\n\n rows = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .all()\n )\n\n summary: dict[str, dict[str, Any]] = {}\n for row in rows:\n conv_id = row.conversation_id\n if not conv_id:\n continue\n created_at = row.created_at or datetime.now()\n if conv_id not in summary:\n summary[conv_id] = {\n \"conversation_id\": conv_id,\n \"title\": \"\",\n \"updated_at\": created_at,\n \"last_message\": row.text,\n \"last_role\": row.role,\n \"last_state\": row.state,\n \"last_task_id\": row.task_id,\n \"message_count\": 0,\n }\n item = summary[conv_id]\n item[\"message_count\"] += 1\n if row.role == \"user\" and row.text and not item[\"title\"]:\n item[\"title\"] = row.text.strip()[:80]\n\n items = []\n search_term = search.lower().strip() if search else \"\"\n archived_total = sum(\n 1\n for c in summary.values()\n if _is_conversation_archived(c.get(\"updated_at\"))\n )\n active_total = len(summary) - archived_total\n for conv in summary.values():\n conv[\"archived\"] = _is_conversation_archived(conv.get(\"updated_at\"))\n if not conv.get(\"title\"):\n conv[\"title\"] = f\"Conversation {conv['conversation_id'][:8]}\"\n if search_term:\n haystack = (\n f\"{conv.get('title', '')} {conv.get('last_message', '')}\".lower()\n )\n if search_term not in haystack:\n continue\n if archived_only and not conv[\"archived\"]:\n continue\n if not archived_only and not include_archived and conv[\"archived\"]:\n continue\n updated = conv.get(\"updated_at\")\n conv[\"updated_at\"] = (\n updated.isoformat() if isinstance(updated, datetime) else None\n )\n items.append(conv)\n\n items.sort(key=lambda x: x.get(\"updated_at\") or \"\", reverse=True)\n total = len(items)\n start = (page - 1) * page_size\n page_items = items[start : start + page_size]\n\n return {\n \"items\": page_items,\n \"total\": total,\n \"page\": page,\n \"page_size\": page_size,\n \"has_next\": start + page_size < total,\n \"active_total\": active_total,\n \"archived_total\": archived_total,\n }\n\n\n# #endregion list_conversations\n" }, { "contract_id": "delete_conversation", @@ -5241,7 +5259,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_command_parser.py", "start_line": 1, - "end_line": 90, + "end_line": 108, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -5261,14 +5279,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantCommandParser [C:4] [TYPE Module] [SEMANTICS assistant, command, parser, nlu, intent]\n# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @INVARIANT Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\n\nfrom ._resolvers import _extract_id, _is_production_env\n\n\n# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT None (pure parsing logic).\n# @PRE message contains raw user text and config manager resolves environments.\n# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n\n\n# #endregion AssistantCommandParser\n" + "body": "# #region AssistantCommandParser [C:4] [TYPE Module] [SEMANTICS assistant, command, parser, nlu, intent]\n# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @INVARIANT Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\n\nfrom __future__ import annotations\n\nimport re\nfrom typing import Any\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\n\nfrom ._resolvers import _extract_id, _is_production_env\n\n\n# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT None (pure parsing logic).\n# @PRE message contains raw user text and config manager resolves environments.\n# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n # ── Новые инструменты ────────────────────────────────────────\n if any(k in lower for k in ['список дашборд', 'покажи дашборд', 'найди дашборд', 'list dashboard', 'search dashboard', 'all dashboards', 'все дашборд']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n search_phrase = _extract_id(lower, ['(?:найди|search|поиск)\\\\s+([а-яa-z0-9_-]{2,})'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'dashboards', 'operation': 'search_dashboards', 'entities': {'environment': env_match, 'search': search_phrase}, 'confidence': 0.88 if env_match else 0.75, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['окружени', 'environment', 'среды', 'сред']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'environments', 'operation': 'list_environments', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['обслуживан', 'maintenance', 'события']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'maintenance', 'operation': 'list_maintenance_events', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['llm провайдер', 'llm provider', 'провайдеры']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'list_llm_providers', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['llm статус', 'llm status', 'проверь llm', 'llm провер']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'get_llm_status', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n\n\n# #endregion AssistantCommandParser\n" }, { "contract_id": "_parse_command", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_command_parser.py", "start_line": 18, - "end_line": 87, + "end_line": 105, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -5297,7 +5315,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT None (pure parsing logic).\n# @PRE message contains raw user text and config manager resolves environments.\n# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n" + "body": "# #region _parse_command [C:4] [TYPE Function]\n# @BRIEF Deterministically parse RU/EN command text into intent payload.\n# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]\n# @RELATION DEPENDS_ON -> [_extract_id]\n# @RELATION DEPENDS_ON -> [_is_production_env]\n# @SIDE_EFFECT None (pure parsing logic).\n# @PRE message contains raw user text and config manager resolves environments.\n# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.\n# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.\ndef _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:\n with belief_scope('_parse_command'):\n logger.reason('Belief protocol reasoning checkpoint for _parse_command')\n text = message.strip()\n lower = text.lower()\n if any(phrase in lower for phrase in ['что ты умеешь', 'что умеешь', 'что ты можешь', 'help', 'помощь', 'доступные команды', 'какие команды']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'assistant', 'operation': 'show_capabilities', 'entities': {}, 'confidence': 0.98, 'risk_level': 'safe', 'requires_confirmation': False}\n dashboard_id = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n dashboard_ref = _extract_id(lower, ['(?:дашборд\\\\w*|dashboard)\\\\s*(?:id\\\\s*)?([a-zа-я0-9._-]+)'])\n dataset_id = _extract_id(lower, ['(?:датасет\\\\w*|dataset)\\\\s*(?:id\\\\s*)?(\\\\d+)'])\n task_id = _extract_id(lower, ['(task[-_a-z0-9]{1,}|[0-9a-f]{8}-[0-9a-f-]{27,})'])\n if any(k in lower for k in ['статус', 'status', 'state', 'проверь задачу']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'status', 'operation': 'get_task_status', 'entities': {'task_id': task_id}, 'confidence': 0.92 if task_id else 0.66, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['ветк', 'branch']) and any(k in lower for k in ['созд', 'сделай', 'create']):\n branch = _extract_id(lower, ['(?:ветк\\\\w*|branch)\\\\s+([a-z0-9._/-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'create_branch', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'branch_name': branch}, 'confidence': 0.95 if branch and dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['коммит', 'commit']):\n quoted = re.search('\"([^\"]{3,120})\"', text)\n message_text = quoted.group(1) if quoted else 'assistant: update dashboard changes'\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'commit_changes', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'message': message_text}, 'confidence': 0.9 if dashboard_id else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['деплой', 'deploy', 'разверн']):\n env_match = _extract_id(lower, ['(?:в|to)\\\\s+([a-z0-9_-]+)'])\n is_dangerous = _is_production_env(env_match, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'git', 'operation': 'deploy_dashboard', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.92 if dashboard_id and env_match else 0.7, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous}\n if any(k in lower for k in ['миграц', 'migration', 'migrate']):\n src = _extract_id(lower, ['(?:с|from)\\\\s+([a-z0-9_-]+)'])\n tgt = _extract_id(lower, ['(?:на|to)\\\\s+([a-z0-9_-]+)'])\n dry_run = '--dry-run' in lower or 'dry run' in lower\n replace_db_config = '--replace-db-config' in lower\n fix_cross_filters = '--no-fix-cross-filters' not in lower\n is_dangerous = _is_production_env(tgt, config_manager)\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'migration', 'operation': 'execute_migration', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'source_env': src, 'target_env': tgt, 'dry_run': dry_run, 'replace_db_config': replace_db_config, 'fix_cross_filters': fix_cross_filters}, 'confidence': 0.95 if dashboard_id and src and tgt else 0.72, 'risk_level': 'dangerous' if is_dangerous else 'guarded', 'requires_confirmation': is_dangerous or dry_run}\n if any(k in lower for k in ['бэкап', 'backup', 'резерв']):\n env_match = _extract_id(lower, ['(?:в|for|из|from)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'backup', 'operation': 'run_backup', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'environment': env_match}, 'confidence': 0.9 if env_match else 0.7, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['здоровье', 'health', 'ошибки', 'failing', 'проблемы']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'health', 'operation': 'get_health_summary', 'entities': {'environment': env_match}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['валидац', 'validate', 'провер']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_validation', 'entities': {'dashboard_id': int(dashboard_id) if dashboard_id else None, 'dashboard_ref': dashboard_ref if dashboard_ref and (not dashboard_ref.isdigit()) else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dashboard_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n if any(k in lower for k in ['документац', 'documentation', 'generate docs', 'сгенерируй док']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n provider_match = _extract_id(lower, ['(?:provider|провайдер)\\\\s+([a-z0-9_-]+)'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'run_llm_documentation', 'entities': {'dataset_id': int(dataset_id) if dataset_id else None, 'environment': env_match, 'provider': provider_match}, 'confidence': 0.88 if dataset_id else 0.64, 'risk_level': 'guarded', 'requires_confirmation': False}\n # ── Новые инструменты ────────────────────────────────────────\n if any(k in lower for k in ['список дашборд', 'покажи дашборд', 'найди дашборд', 'list dashboard', 'search dashboard', 'all dashboards', 'все дашборд']):\n env_match = _extract_id(lower, ['(?:в|for|env|окружени[ея])\\\\s+([a-z0-9_-]+)'])\n search_phrase = _extract_id(lower, ['(?:найди|search|поиск)\\\\s+([а-яa-z0-9_-]{2,})'])\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'dashboards', 'operation': 'search_dashboards', 'entities': {'environment': env_match, 'search': search_phrase}, 'confidence': 0.88 if env_match else 0.75, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['окружени', 'environment', 'среды', 'сред']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'environments', 'operation': 'list_environments', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['обслуживан', 'maintenance', 'события']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'maintenance', 'operation': 'list_maintenance_events', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['llm провайдер', 'llm provider', 'провайдеры']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'list_llm_providers', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n if any(k in lower for k in ['llm статус', 'llm status', 'проверь llm', 'llm провер']):\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'llm', 'operation': 'get_llm_status', 'entities': {}, 'confidence': 0.9, 'risk_level': 'safe', 'requires_confirmation': False}\n logger.reflect('Belief protocol postcondition checkpoint for _parse_command')\n return {'domain': 'unknown', 'operation': 'clarify', 'entities': {}, 'confidence': 0.3, 'risk_level': 'safe', 'requires_confirmation': False}\n\n\n# #endregion _parse_command\n" }, { "contract_id": "AssistantDatasetReview", @@ -5510,7 +5528,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, dispatch, confirm]\n# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT Dataset review dispatch requires valid session version for write operations.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ApprovalState,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.schemas.auth import User\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n PreparePreviewCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\nfrom ._schemas import (\n AssistantAction,\n)\n\n\n# #region _dataset_review_conflict_http_exception [C:2] [TYPE Function]\n# @BRIEF Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# #endregion _dataset_review_conflict_http_exception\n\n\n# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE context contains valid session data and user intent.\n# @POST Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n\n\n# #endregion AssistantDatasetReviewDispatch\n" + "body": "# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, dispatch, confirm]\n# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT Dataset review dispatch requires valid session version for write operations.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.dataset_review import FieldSemanticUpdateRequest, _update_semantic_field_state\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ApprovalState,\n ReadinessState,\n RecommendedAction,\n)\nfrom src.schemas.auth import User\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n PreparePreviewCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\nfrom ._schemas import (\n AssistantAction,\n)\n\n\n# #region _dataset_review_conflict_http_exception [C:2] [TYPE Function]\n# @BRIEF Convert dataset-review optimistic-lock conflicts into shared 409 assistant semantics.\ndef _dataset_review_conflict_http_exception(\n exc: DatasetReviewSessionVersionConflictError,\n) -> HTTPException:\n return HTTPException(\n status_code=status.HTTP_409_CONFLICT,\n detail={\n \"error_code\": \"session_version_conflict\",\n \"message\": str(exc),\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n },\n )\n\n\n# #endregion _dataset_review_conflict_http_exception\n\n\n# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE context contains valid session data and user intent.\n# @POST Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.now(UTC)\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n\n\n# #endregion AssistantDatasetReviewDispatch\n" }, { "contract_id": "_dataset_review_conflict_http_exception", @@ -5556,79 +5574,66 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE context contains valid session data and user intent.\n# @POST Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.utcnow()\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n" + "body": "# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]\n# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.\n# @RELATION CALLS -> DatasetReviewOrchestrator\n# @PRE context contains valid session data and user intent.\n# @POST Returns a structured response with planned actions and confirmations.\n# @SIDE_EFFECT May update session state and enqueue tasks.\nasync def _dispatch_dataset_review_intent(\n intent: dict[str, Any],\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n with belief_scope(\"_dispatch_dataset_review_intent\"):\n logger.reason(\n \"Dispatching assistant dataset-review intent\",\n extra={\"operation\": intent.get(\"operation\")},\n )\n entities = intent.get(\"entities\", {})\n session_id = entities.get(\"dataset_review_session_id\")\n session_version = entities.get(\"session_version\")\n if not session_id or session_version is None:\n raise HTTPException(\n status_code=422,\n detail=\"Missing dataset_review_session_id/session_version\",\n )\n\n operation = str(intent.get(\"operation\") or \"\")\n repository = DatasetReviewSessionRepository(db)\n if operation == \"dataset_review_answer_context\":\n summary = str(entities.get(\"summary\") or \"\")\n logger.reflect(\n \"Returned assistant-safe dataset review context summary\",\n extra={\"session_id\": session_id, \"operation\": operation},\n )\n return summary, None, []\n\n session = repository.load_session_detail(session_id, current_user.id)\n if session is None or session.user_id != current_user.id:\n logger.explore(\n \"Assistant dataset-review intent rejected because session was not found\",\n extra={\"session_id\": session_id, \"user_id\": current_user.id},\n )\n raise HTTPException(\n status_code=404, detail=\"Dataset review session not found\"\n )\n\n try:\n repository.require_session_version(session, int(session_version))\n except DatasetReviewSessionVersionConflictError as exc:\n logger.explore(\n \"Assistant dataset-review intent rejected due to stale session version\",\n extra={\n \"session_id\": exc.session_id,\n \"expected_version\": exc.expected_version,\n \"actual_version\": exc.actual_version,\n \"operation\": operation,\n },\n )\n raise _dataset_review_conflict_http_exception(exc) from exc\n\n logger.reason(\n \"Dispatching confirmed assistant dataset-review intent\",\n extra={\n \"session_id\": session_id,\n \"session_version\": session_version,\n \"operation\": operation,\n },\n )\n\n if operation == \"dataset_review_approve_mappings\":\n mapping_ids = list(dict.fromkeys(entities.get(\"mapping_ids\") or []))\n if not mapping_ids:\n raise HTTPException(\n status_code=409, detail=\"No pending mappings to approve\"\n )\n updated_count = 0\n for mapping in session.execution_mappings:\n if mapping.mapping_id not in mapping_ids:\n continue\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n updated_count += 1\n if updated_count == 0:\n raise HTTPException(\n status_code=409, detail=\"No matching mappings available to approve\"\n )\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_mapping_approval\",\n event_summary=\"Assistant-approved warning-sensitive mappings persisted\",\n event_details={\n \"mapping_ids\": mapping_ids,\n \"count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant mapping approval persisted within optimistic-lock boundary\",\n extra={\n \"session_id\": session_id,\n \"updated_count\": updated_count,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Approved {updated_count} mapping(s) for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open mapping review\",\n target=\"mapping\",\n )\n ],\n )\n\n if operation == \"dataset_review_set_field_semantics\":\n field_id = str(entities.get(\"field_id\") or \"\").strip()\n if not field_id:\n raise HTTPException(status_code=422, detail=\"Missing field_id\")\n field = next(\n (item for item in session.semantic_fields if item.field_id == field_id),\n None,\n )\n if field is None:\n raise HTTPException(status_code=404, detail=\"Semantic field not found\")\n update_request = FieldSemanticUpdateRequest(\n candidate_id=entities.get(\"candidate_id\"),\n verbose_name=entities.get(\"verbose_name\"),\n description=entities.get(\"description\"),\n display_format=entities.get(\"display_format\"),\n lock_field=bool(entities.get(\"lock_field\", False)),\n )\n try:\n _update_semantic_field_state(\n field, update_request, changed_by=\"assistant\"\n )\n except HTTPException:\n raise\n except ValueError as exc:\n raise HTTPException(status_code=400, detail=str(exc)) from exc\n session.last_activity_at = datetime.now(UTC)\n repository.bump_session_version(session)\n repository.db.commit()\n repository.db.refresh(session)\n repository.db.refresh(field)\n repository.event_logger.log_for_session(\n session,\n actor_user_id=current_user.id,\n event_type=\"assistant_field_semantics_updated\",\n event_summary=\"Assistant semantic field update persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"candidate_id\": entities.get(\"candidate_id\"),\n \"lock_field\": bool(entities.get(\"lock_field\", False)),\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n logger.reflect(\n \"Assistant semantic field update committed safely\",\n extra={\n \"session_id\": session_id,\n \"field_id\": field_id,\n \"version\": int(getattr(session, \"version\", 0) or 0),\n },\n )\n return (\n f\"Updated semantic field {field.field_name} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open semantic review\",\n target=f\"field:{field.field_id}\",\n )\n ],\n )\n\n if operation == \"dataset_review_generate_sql_preview\":\n orchestrator = DatasetReviewOrchestrator(\n repository=repository,\n config_manager=config_manager,\n )\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=int(session_version),\n )\n )\n preview_status = getattr(\n result.preview.preview_status, \"value\", result.preview.preview_status\n )\n logger.reflect(\n \"Assistant-triggered Superset preview generation completed\",\n extra={\n \"session_id\": session_id,\n \"preview_status\": preview_status,\n },\n )\n return (\n f\"SQL preview {preview_status} for dataset review session {session_id}.\",\n None,\n [\n AssistantAction(\n type=\"focus_target\",\n label=\"Open SQL preview\",\n target=\"sql-preview\",\n )\n ],\n )\n\n raise HTTPException(\n status_code=400, detail=\"Unsupported dataset review operation\"\n )\n\n\n# #endregion _dispatch_dataset_review_intent\n" }, { "contract_id": "AssistantDispatch", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_dispatch.py", "start_line": 1, - "end_line": 317, - "tier": "TIER_3", - "complexity": 5, + "end_line": 215, + "tier": "TIER_2", + "complexity": 4, "metadata": { - "BRIEF": "Intent dispatch engine, confirmation summary, and clarification text for the assistant API.", - "COMPLEXITY": 5, - "INVARIANT": "Unsupported operations are rejected via HTTPException(400).", + "BRIEF": "Intent dispatch engine and backward-compat wrapper around the central tool registry.", + "COMPLEXITY": 4, + "INVARIANT": "All tool handlers live in _tool_registry — _dispatch is a thin wrapper.", "LAYER": "API" }, "relations": [ + { + "source_id": "AssistantDispatch", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, { "source_id": "AssistantDispatch", "relation_type": "DEPENDS_ON", "target_id": "AssistantSchemas", "target_ref": "[AssistantSchemas]" - }, - { - "source_id": "AssistantDispatch", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantResolvers", - "target_ref": "[AssistantResolvers]" - }, - { - "source_id": "AssistantDispatch", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantLlmPlanner", - "target_ref": "[AssistantLlmPlanner]" - }, - { - "source_id": "AssistantDispatch", - "relation_type": "DEPENDS_ON", - "target_id": "AssistantDatasetReview", - "target_ref": "[AssistantDatasetReview]" } ], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantDispatch [C:5] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration]\n# @BRIEF Intent dispatch engine, confirmation summary, and clarification text for the assistant API.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @INVARIANT Unsupported operations are rejected via HTTPException(400).\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.git_service import GitService\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._dataset_review_dispatch import _dispatch_dataset_review_intent\nfrom ._history import _coerce_query_bool\nfrom ._llm_planner import _check_any_permission\nfrom ._resolvers import (\n _build_task_observability_summary,\n _extract_result_deep_links,\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n _resolve_provider_id,\n)\nfrom ._schemas import (\n _DATASET_REVIEW_OPS,\n AssistantAction,\n)\n\n# #region _get_git_service [TYPE Function]\n# @BRIEF Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable.\n# @POST Returns GitService instance (created once, cached).\n# @SIDE_EFFECT May attempt to create /app/storage/repositories on first call.\n# @RATIONALE Module-level GitService() instantiation crashed test collection for 33+ test files\n# because /app/storage/ doesn't exist outside Docker — see test fix.\n_git_service_instance: GitService | None = None\n\ndef _get_git_service() -> GitService:\n global _git_service_instance\n if _git_service_instance is None:\n _git_service_instance = GitService()\n return _git_service_instance\n# #endregion _get_git_service\n\n\n# #region _clarification_text_for_intent [C:2] [TYPE Function]\n# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE state was classified as needs_clarification for current intent/error combination.\n# @POST Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: dict[str, Any] | None, detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# #endregion _clarification_text_for_intent\n\n\n# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE actions is a non-empty list of planned review actions.\n# @POST Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], 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 service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n\n\n# #region _dispatch_intent [C:5] [TYPE Function]\n# @BRIEF 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, str | None, 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 _get_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 _get_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 if not provider or not bool(provider.is_multimodal):\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# #endregion _dispatch_intent\n\n\n# #endregion AssistantDispatch\n" + "body": "# #region AssistantDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration]\n# @BRIEF Intent dispatch engine and backward-compat wrapper around the central tool registry.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT Unsupported operations are rejected via HTTPException(400).\n# @INVARIANT All tool handlers live in _tool_registry — _dispatch is a thin wrapper.\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.git_service import GitService\n\nfrom ._history import _coerce_query_bool\nfrom ._resolvers import (\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n)\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import dispatch as _registry_dispatch\n\n# #region _get_git_service [TYPE Function]\n# @BRIEF Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable.\n# @POST Returns GitService instance (created once, cached).\n# @SIDE_EFFECT May attempt to create /app/storage/repositories on first call.\n_git_service_instance: GitService | None = None\n\n\ndef _get_git_service() -> GitService:\n global _git_service_instance\n if _git_service_instance is None:\n _git_service_instance = GitService()\n return _git_service_instance\n\n\n# #endregion _get_git_service\n\n\n# #region _clarification_text_for_intent [C:2] [TYPE Function]\n# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.\n# @PRE state was classified as needs_clarification for current intent/error combination.\n# @POST Returned text is human-readable and actionable for target operation.\ndef _clarification_text_for_intent(\n intent: dict[str, Any] | None, detail_text: str\n) -> str:\n operation = (intent or {}).get(\"operation\")\n guidance_by_operation: dict[str, str] = {\n \"run_llm_validation\": (\n \"Нужно уточнение для запуска LLM-валидации: Укажите дашборд (id или slug), окружение и провайдер LLM.\"\n ),\n \"run_llm_documentation\": (\n \"Нужно уточнение для генерации документации: Укажите dataset_id, окружение и провайдер LLM.\"\n ),\n \"create_branch\": \"Нужно уточнение: укажите дашборд (id/slug/title) и имя ветки.\",\n \"commit_changes\": \"Нужно уточнение: укажите дашборд (id/slug/title) для коммита.\",\n \"deploy_dashboard\": \"Нужно уточнение: укажите дашборд (id/slug/title) и целевое окружение.\",\n \"execute_migration\": \"Нужно уточнение: укажите дашборд (id/slug/title), source_env и target_env.\",\n \"run_backup\": \"Нужно уточнение: укажите окружение и при необходимости дашборд (id/slug/title).\",\n }\n return guidance_by_operation.get(operation, detail_text)\n\n\n# #endregion _clarification_text_for_intent\n\n\n# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE actions is a non-empty list of planned review actions.\n# @POST Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, tuple[str, str]] = {\n 'create_branch': (\n '📝 Создание ветки в Git',\n 'Будет создана ветка **{branch}** для дашборда **{dashboard}**. '\n 'Изменения можно будет коммитить в эту ветку.'\n ),\n 'commit_changes': (\n '📝 Коммит изменений в Git',\n 'Изменения дашборда **{dashboard}** будут закоммичены в Git-репозиторий. '\n 'После коммита их можно будет развернуть на других окружениях.'\n ),\n 'deploy_dashboard': (\n '🚀 Деплой дашборда',\n 'Дашборд **{dashboard}** будет развёрнут в окружение **{env}**. '\n 'Изменения станут доступны на целевом окружении.'\n ),\n 'execute_migration': (\n '🔄 Миграция дашборда',\n 'Дашборд **{dashboard}** будет перенесён с **{src}** на **{tgt}**.'\n ),\n 'run_backup': (\n '💾 Бэкап',\n 'Будет запущено резервное копирование окружения **{env}**{dashboard}.'\n ),\n 'run_llm_validation': (\n '🤖 LLM-валидация дашборда',\n 'Будет запущена LLM-валидация дашборда **{dashboard}**{env}. '\n 'Это займёт некоторое время.'\n ),\n 'run_llm_documentation': (\n '📄 Генерация документации',\n 'Будет сгенерирована документация для датасета **{dataset}**{env}.'\n ),\n 'start_maintenance': (\n '🛠 Обслуживание',\n 'Будет запущено окно обслуживания для таблиц **{tables}**.'\n ),\n 'end_maintenance': (\n '✅ Завершение обслуживания',\n 'Обслуживание будет завершено{event}.'\n ),\n }\n entry = descriptions.get(operation)\n if not entry:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n title, desc = entry\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n env_val = entities.get('environment') or entities.get('target_env')\n dataset_val = entities.get('dataset_id')\n event_val = entities.get('event_id')\n tables_val = entities.get('tables')\n tables_str = ', '.join(tables_val[:5]) if isinstance(tables_val, list) else str(tables_val or '')\n text = desc.format(\n branch=_label(entities.get('branch_name')),\n dashboard=_label(dashboard),\n env=_label(env_val),\n src=_label(entities.get('source_env')),\n tgt=_label(entities.get('target_env')),\n dataset=_label(dataset_val),\n tables=tables_str if tables_str else '?',\n event=_label(event_val),\n )\n text = f'**{title}**\\n\\n{text}'\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], 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 service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n\n\n# #region _dispatch_intent [C:1] [TYPE Function]\n# @BRIEF Backward-compat wrapper around the central tool registry dispatch.\n# @DEPRECATED Use `dispatch()` from `_tool_registry` directly.\nasync def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Backward-compat wrapper around the central tool registry dispatch.\"\"\"\n operation = intent.get('operation')\n return await _registry_dispatch(operation, intent, current_user, task_manager, config_manager, db)\n\n\n# #endregion _dispatch_intent\n\n\n# #endregion AssistantDispatch\n" }, { "contract_id": "_get_git_service", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 40, - "end_line": 53, + "start_line": 31, + "end_line": 45, "tier": "TIER_1", "complexity": 1, "metadata": { "BRIEF": "Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable.", "POST": "Returns GitService instance (created once, cached).", - "RATIONALE": "Module-level GitService() instantiation crashed test collection for 33+ test files", "SIDE_EFFECT": "May attempt to create /app/storage/repositories on first call." }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _get_git_service [TYPE Function]\n# @BRIEF Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable.\n# @POST Returns GitService instance (created once, cached).\n# @SIDE_EFFECT May attempt to create /app/storage/repositories on first call.\n# @RATIONALE Module-level GitService() instantiation crashed test collection for 33+ test files\n# because /app/storage/ doesn't exist outside Docker — see test fix.\n_git_service_instance: GitService | None = None\n\ndef _get_git_service() -> GitService:\n global _git_service_instance\n if _git_service_instance is None:\n _git_service_instance = GitService()\n return _git_service_instance\n# #endregion _get_git_service\n" + "body": "# #region _get_git_service [TYPE Function]\n# @BRIEF Lazy-init GitService singleton to avoid crash at module import time when /app/storage/ is unavailable.\n# @POST Returns GitService instance (created once, cached).\n# @SIDE_EFFECT May attempt to create /app/storage/repositories on first call.\n_git_service_instance: GitService | None = None\n\n\ndef _get_git_service() -> GitService:\n global _git_service_instance\n if _git_service_instance is None:\n _git_service_instance = GitService()\n return _git_service_instance\n\n\n# #endregion _get_git_service\n" }, { "contract_id": "_clarification_text_for_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 56, - "end_line": 80, + "start_line": 48, + "end_line": 72, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5647,8 +5652,8 @@ "contract_id": "_async_confirmation_summary", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 83, - "end_line": 150, + "start_line": 75, + "end_line": 200, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -5662,62 +5667,33 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE actions is a non-empty list of planned review actions.\n# @POST Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, str] = {'create_branch': 'создание ветки{branch} для дашборда{dashboard}', 'commit_changes': 'коммит изменений для дашборда{dashboard}', 'deploy_dashboard': 'деплой дашборда{dashboard} в окружение{env}', 'execute_migration': 'миграция дашборда{dashboard} с{src} на{tgt}', 'run_backup': 'бэкап окружения{env}{dashboard}', 'run_llm_validation': 'LLM-валидация дашборда{dashboard}{env}', 'run_llm_documentation': 'генерация документации для датасета{dataset}{env}'}\n template = descriptions.get(operation)\n if not template:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n text = template.format(branch=_label(entities.get('branch_name')), dashboard=_label(dashboard), env=_label(entities.get('environment') or entities.get('target_env')), src=_label(entities.get('source_env')), tgt=_label(entities.get('target_env')), dataset=_label(entities.get('dataset_id')))\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], 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 service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n" + "body": "# #region _async_confirmation_summary [C:4] [TYPE Function]\n# @BRIEF Build human-readable confirmation prompt for an intent before execution.\n# @PRE actions is a non-empty list of planned review actions.\n# @POST Returns a formatted summary string suitable for display to the user.\n# @SIDE_EFFECT None - pure formatting function.\nasync def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:\n with belief_scope('_confirmation_summary'):\n logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')\n operation = intent.get('operation', '')\n entities = intent.get('entities', {})\n descriptions: dict[str, tuple[str, str]] = {\n 'create_branch': (\n '📝 Создание ветки в Git',\n 'Будет создана ветка **{branch}** для дашборда **{dashboard}**. '\n 'Изменения можно будет коммитить в эту ветку.'\n ),\n 'commit_changes': (\n '📝 Коммит изменений в Git',\n 'Изменения дашборда **{dashboard}** будут закоммичены в Git-репозиторий. '\n 'После коммита их можно будет развернуть на других окружениях.'\n ),\n 'deploy_dashboard': (\n '🚀 Деплой дашборда',\n 'Дашборд **{dashboard}** будет развёрнут в окружение **{env}**. '\n 'Изменения станут доступны на целевом окружении.'\n ),\n 'execute_migration': (\n '🔄 Миграция дашборда',\n 'Дашборд **{dashboard}** будет перенесён с **{src}** на **{tgt}**.'\n ),\n 'run_backup': (\n '💾 Бэкап',\n 'Будет запущено резервное копирование окружения **{env}**{dashboard}.'\n ),\n 'run_llm_validation': (\n '🤖 LLM-валидация дашборда',\n 'Будет запущена LLM-валидация дашборда **{dashboard}**{env}. '\n 'Это займёт некоторое время.'\n ),\n 'run_llm_documentation': (\n '📄 Генерация документации',\n 'Будет сгенерирована документация для датасета **{dataset}**{env}.'\n ),\n 'start_maintenance': (\n '🛠 Обслуживание',\n 'Будет запущено окно обслуживания для таблиц **{tables}**.'\n ),\n 'end_maintenance': (\n '✅ Завершение обслуживания',\n 'Обслуживание будет завершено{event}.'\n ),\n }\n entry = descriptions.get(operation)\n if not entry:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return 'Подтвердите выполнение операции или отмените.'\n\n title, desc = entry\n\n def _label(value: Any, prefix: str=' ') -> str:\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'{prefix}{value}' if value else ''\n dashboard = entities.get('dashboard_id') or entities.get('dashboard_ref')\n env_val = entities.get('environment') or entities.get('target_env')\n dataset_val = entities.get('dataset_id')\n event_val = entities.get('event_id')\n tables_val = entities.get('tables')\n tables_str = ', '.join(tables_val[:5]) if isinstance(tables_val, list) else str(tables_val or '')\n text = desc.format(\n branch=_label(entities.get('branch_name')),\n dashboard=_label(dashboard),\n env=_label(env_val),\n src=_label(entities.get('source_env')),\n tgt=_label(entities.get('target_env')),\n dataset=_label(dataset_val),\n tables=tables_str if tables_str else '?',\n event=_label(event_val),\n )\n text = f'**{title}**\\n\\n{text}'\n if operation == 'execute_migration':\n flags = []\n flags.append('маппинг БД: ' + ('ВКЛ' if _coerce_query_bool(entities.get('replace_db_config', False)) else 'ВЫКЛ'))\n flags.append('исправление кроссфильтров: ' + ('ВКЛ' if _coerce_query_bool(entities.get('fix_cross_filters', True)) else 'ВЫКЛ'))\n dry_run_enabled = _coerce_query_bool(entities.get('dry_run', False))\n flags.append('отчет dry-run: ' + ('ВКЛ' if dry_run_enabled else 'ВЫКЛ'))\n text += f\" ({', '.join(flags)})\"\n if dry_run_enabled:\n try:\n from src.core.migration.dry_run_orchestrator import MigrationDryRunService\n from src.core.superset_client import SupersetClient\n from src.models.dashboard import DashboardSelection\n src_token = entities.get('source_env')\n tgt_token = entities.get('target_env')\n dashboard_id = _resolve_dashboard_id_entity(entities, config_manager, env_hint=src_token)\n if dashboard_id and src_token and tgt_token:\n src_env_id = _resolve_env_id(src_token, config_manager)\n tgt_env_id = _resolve_env_id(tgt_token, config_manager)\n if src_env_id and tgt_env_id:\n env_map = {env.id: env for env in config_manager.get_environments()}\n source_env = env_map.get(src_env_id)\n target_env = env_map.get(tgt_env_id)\n if source_env and target_env and (source_env.id != target_env.id):\n selection = DashboardSelection(source_env_id=source_env.id, target_env_id=target_env.id, selected_ids=[dashboard_id], 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 service = MigrationDryRunService()\n source_client = SupersetClient(source_env)\n target_client = SupersetClient(target_env)\n report = service.run(selection, source_client, target_client, db)\n s = report.get('summary', {})\n dash_s = s.get('dashboards', {})\n charts_s = s.get('charts', {})\n ds_s = s.get('datasets', {})\n creates = dash_s.get('create', 0) + charts_s.get('create', 0) + ds_s.get('create', 0)\n updates = dash_s.get('update', 0) + charts_s.get('update', 0) + ds_s.get('update', 0)\n deletes = dash_s.get('delete', 0) + charts_s.get('delete', 0) + ds_s.get('delete', 0)\n text += f'\\n\\nОтчет dry-run:\\n- Будет создано новых объектов: {creates}\\n- Будет обновлено: {updates}\\n- Будет удалено: {deletes}'\n else:\n text += '\\n\\n(Не удалось загрузить отчет dry-run: неверные окружения).'\n except Exception as e:\n import traceback\n logger.warning('[assistant.dry_run_summary][failed] Exception: %s\\n%s', e, traceback.format_exc())\n text += f'\\n\\n(Не удалось загрузить отчет dry-run: {e}).'\n logger.reflect('Belief protocol postcondition checkpoint for _confirmation_summary')\n return f'Выполнить: {text}. Подтвердите или отмените.'\n\n\n# #endregion _async_confirmation_summary\n" }, { "contract_id": "_dispatch_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_dispatch.py", - "start_line": 153, - "end_line": 314, - "tier": "TIER_3", - "complexity": 5, + "start_line": 203, + "end_line": 212, + "tier": "TIER_1", + "complexity": 1, "metadata": { - "BRIEF": "Execute parsed assistant intent via existing task/plugin/git services.", - "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.", - "PRE": "intent operation is known and actor permissions are validated per operation.", - "SIDE_EFFECT": "May enqueue tasks, invoke git operations, and query/update external service state." + "BRIEF": "Backward-compat wrapper around the central tool registry dispatch.", + "COMPLEXITY": 1, + "DEPRECATED": "Use `dispatch()` from `_tool_registry` directly." }, - "relations": [ - { - "source_id": "_dispatch_intent", - "relation_type": "DEPENDS_ON", - "target_id": "_check_any_permission", - "target_ref": "[_check_any_permission]" - }, - { - "source_id": "_dispatch_intent", - "relation_type": "DEPENDS_ON", - "target_id": "_resolve_dashboard_id_entity", - "target_ref": "[_resolve_dashboard_id_entity]" - }, - { - "source_id": "_dispatch_intent", - "relation_type": "DEPENDS_ON", - "target_id": "TaskManager", - "target_ref": "[TaskManager]" - }, - { - "source_id": "_dispatch_intent", - "relation_type": "DEPENDS_ON", - "target_id": "GitService", - "target_ref": "[GitService]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _dispatch_intent [C:5] [TYPE Function]\n# @BRIEF 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, str | None, 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 _get_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 _get_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 if not provider or not bool(provider.is_multimodal):\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# #endregion _dispatch_intent\n" + "body": "# #region _dispatch_intent [C:1] [TYPE Function]\n# @BRIEF Backward-compat wrapper around the central tool registry dispatch.\n# @DEPRECATED Use `dispatch()` from `_tool_registry` directly.\nasync def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Backward-compat wrapper around the central tool registry dispatch.\"\"\"\n operation = intent.get('operation')\n return await _registry_dispatch(operation, intent, current_user, task_manager, config_manager, db)\n\n\n# #endregion _dispatch_intent\n" }, { "contract_id": "AssistantHistory", "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_history.py", "start_line": 1, - "end_line": 369, + "end_line": 374, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5737,7 +5713,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]\n# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT Failed persistence attempts always rollback before returning.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime, timedelta\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import logger\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\nfrom ._schemas import (\n ASSISTANT_ARCHIVE_AFTER_DAYS,\n ASSISTANT_AUDIT,\n ASSISTANT_MESSAGE_TTL_DAYS,\n CONVERSATIONS,\n ConfirmationRecord,\n)\n\nlogger = logger\n\n\n# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION BINDS_TO -> [CONVERSATIONS]\n# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE user_id and conversation_id identify target conversation bucket.\n# @POST Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# #endregion _append_history\n\n\n# #region _persist_message [C:2] [TYPE Function]\n# @BRIEF Persist assistant/user message record to database.\n# @DATA_CONTRACT Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE db session is writable and message payload is serializable.\n# @POST Message row is committed or persistence failure is logged.\n# @INVARIANT failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n metadata: dict[str, Any] | None = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# #endregion _persist_message\n\n\n# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION BINDS_TO -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE payload describes decision/outcome fields.\n# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n\n\n# #region _persist_audit [C:2] [TYPE Function]\n# @BRIEF Persist structured assistant audit payload in database.\n# @PRE db session is writable and payload is JSON-serializable.\n# @POST Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# #endregion _persist_audit\n\n\n# #region _persist_confirmation [C:2] [TYPE Function]\n# @BRIEF Persist confirmation token record to database.\n# @PRE record contains id/user/intent/dispatch/expiry fields.\n# @POST Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# #endregion _persist_confirmation\n\n\n# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE confirmation_id references existing row.\n# @POST State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n\n\n# #region _load_confirmation_from_db [C:2] [TYPE Function]\n# @BRIEF Load confirmation token from database into in-memory model.\n# @PRE confirmation_id may or may not exist in storage.\n# @POST Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> ConfirmationRecord | None:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# #endregion _load_confirmation_from_db\n\n\n# #region _ensure_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation id in memory or create a new one.\n# @PRE user_id identifies current actor.\n# @POST Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: str | None) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _ensure_conversation\n\n\n# #region _resolve_or_create_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE user_id and db session are available.\n# @POST Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: str | None, db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _resolve_or_create_conversation\n\n\n# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE db session is available and user_id references current actor scope.\n# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n\n\n# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE updated_at can be null for empty conversations.\n# @POST Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# #endregion _is_conversation_archived\n\n\n# #region _coerce_query_bool [C:2] [TYPE Function]\n# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE value may be bool, string, or FastAPI Query metadata object.\n# @POST Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# #endregion _coerce_query_bool\n\n\n# #endregion AssistantHistory\n" + "body": "# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]\n# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @INVARIANT Failed persistence attempts always rollback before returning.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime, timedelta\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.logger import logger\nfrom src.models.assistant import (\n AssistantAuditRecord,\n AssistantConfirmationRecord,\n AssistantMessageRecord,\n)\n\nfrom ._schemas import (\n ASSISTANT_ARCHIVE_AFTER_DAYS,\n ASSISTANT_AUDIT,\n ASSISTANT_MESSAGE_TTL_DAYS,\n CONVERSATIONS,\n ConfirmationRecord,\n)\n\nlogger = logger\n\n\n# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION BINDS_TO -> [CONVERSATIONS]\n# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE user_id and conversation_id identify target conversation bucket.\n# @POST Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.now(UTC),\n }\n )\n\n\n# #endregion _append_history\n\n\n# #region _persist_message [C:2] [TYPE Function]\n# @BRIEF Persist assistant/user message record to database.\n# @DATA_CONTRACT Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]\n# @RELATION DEPENDS_ON -> [AssistantMessageRecord]\n# @SIDE_EFFECT Writes AssistantMessageRecord rows and commits or rollbacks the DB session.\n# @PRE db session is writable and message payload is serializable.\n# @POST Message row is committed or persistence failure is logged.\n# @INVARIANT failed persistence attempts always rollback before returning.\ndef _persist_message(\n db: Session,\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n metadata: dict[str, Any] | None = None,\n):\n try:\n row = AssistantMessageRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n role=role,\n text=text,\n state=state,\n task_id=task_id,\n confirmation_id=confirmation_id,\n payload=metadata,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.message][persist_failed] {exc}\")\n\n\n# #endregion _persist_message\n\n\n# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION BINDS_TO -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE payload describes decision/outcome fields.\n# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.now(UTC).isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n\n\n# #region _persist_audit [C:2] [TYPE Function]\n# @BRIEF Persist structured assistant audit payload in database.\n# @PRE db session is writable and payload is JSON-serializable.\n# @POST Audit row is committed or failure is logged with rollback.\ndef _persist_audit(\n db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None\n):\n try:\n row = AssistantAuditRecord(\n id=str(uuid.uuid4()),\n user_id=user_id,\n conversation_id=conversation_id,\n decision=payload.get(\"decision\"),\n task_id=payload.get(\"task_id\"),\n message=payload.get(\"message\"),\n payload=payload,\n )\n db.add(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.audit][persist_failed] {exc}\")\n\n\n# #endregion _persist_audit\n\n\n# #region _persist_confirmation [C:2] [TYPE Function]\n# @BRIEF Persist confirmation token record to database.\n# @PRE record contains id/user/intent/dispatch/expiry fields.\n# @POST Confirmation row exists in persistent storage.\ndef _persist_confirmation(db: Session, record: ConfirmationRecord):\n try:\n row = AssistantConfirmationRecord(\n id=record.id,\n user_id=record.user_id,\n conversation_id=record.conversation_id,\n state=record.state,\n intent=record.intent,\n dispatch=record.dispatch,\n expires_at=record.expires_at,\n created_at=record.created_at,\n consumed_at=None,\n )\n db.merge(row)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][persist_failed] {exc}\")\n\n\n# #endregion _persist_confirmation\n\n\n# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE confirmation_id references existing row.\n# @POST State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.now(UTC)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n\n\n# #region _load_confirmation_from_db [C:2] [TYPE Function]\n# @BRIEF Load confirmation token from database into in-memory model.\n# @PRE confirmation_id may or may not exist in storage.\n# @POST Returns ConfirmationRecord when found, otherwise None.\ndef _load_confirmation_from_db(\n db: Session, confirmation_id: str\n) -> ConfirmationRecord | None:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return None\n return ConfirmationRecord(\n id=row.id,\n user_id=row.user_id,\n conversation_id=row.conversation_id,\n intent=row.intent or {},\n dispatch=row.dispatch or {},\n expires_at=row.expires_at,\n state=row.state,\n created_at=row.created_at,\n )\n\n\n# #endregion _load_confirmation_from_db\n\n\n# #region _ensure_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation id in memory or create a new one.\n# @PRE user_id identifies current actor.\n# @POST Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.\ndef _ensure_conversation(user_id: str, conversation_id: str | None) -> str:\n if conversation_id:\n from ._schemas import USER_ACTIVE_CONVERSATION\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n from ._schemas import USER_ACTIVE_CONVERSATION\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _ensure_conversation\n\n\n# #region _resolve_or_create_conversation [C:2] [TYPE Function]\n# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.\n# @PRE user_id and db session are available.\n# @POST Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.\ndef _resolve_or_create_conversation(\n user_id: str, conversation_id: str | None, db: Session\n) -> str:\n from ._schemas import USER_ACTIVE_CONVERSATION\n\n if conversation_id:\n USER_ACTIVE_CONVERSATION[user_id] = conversation_id\n return conversation_id\n\n active = USER_ACTIVE_CONVERSATION.get(user_id)\n if active:\n return active\n\n from sqlalchemy import desc\n\n last_message = (\n db.query(AssistantMessageRecord)\n .filter(AssistantMessageRecord.user_id == user_id)\n .order_by(desc(AssistantMessageRecord.created_at))\n .first()\n )\n if last_message:\n USER_ACTIVE_CONVERSATION[user_id] = last_message.conversation_id\n return last_message.conversation_id\n\n new_id = str(uuid.uuid4())\n USER_ACTIVE_CONVERSATION[user_id] = new_id\n return new_id\n\n\n# #endregion _resolve_or_create_conversation\n\n\n# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE db session is available and user_id references current actor scope.\n# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.now() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime):\n # DB stores naive datetimes; in-memory items may be aware (from datetime.now(UTC))\n ref = created_at.replace(tzinfo=None) if created_at.tzinfo else created_at\n if ref < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n\n\n# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE updated_at can be null for empty conversations.\n# @POST Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.now() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n # Ensure both operands are naive (DB stores naive datetimes)\n ref = updated_at.replace(tzinfo=None) if updated_at.tzinfo else updated_at\n return ref < cutoff\n\n\n# #endregion _is_conversation_archived\n\n\n# #region _coerce_query_bool [C:2] [TYPE Function]\n# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.\n# @PRE value may be bool, string, or FastAPI Query metadata object.\n# @POST Returns deterministic boolean flag.\ndef _coerce_query_bool(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, str):\n return value.strip().lower() in {\"1\", \"true\", \"yes\", \"on\"}\n return False\n\n\n# #endregion _coerce_query_bool\n\n\n# #endregion AssistantHistory\n" }, { "contract_id": "_append_history", @@ -5767,7 +5743,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION BINDS_TO -> [CONVERSATIONS]\n# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE user_id and conversation_id identify target conversation bucket.\n# @POST Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.utcnow(),\n }\n )\n\n\n# #endregion _append_history\n" + "body": "# #region _append_history [C:2] [TYPE Function]\n# @BRIEF Append conversation message to in-memory history buffer.\n# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]\n# @RELATION BINDS_TO -> [CONVERSATIONS]\n# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.\n# @PRE user_id and conversation_id identify target conversation bucket.\n# @POST Message entry is appended to CONVERSATIONS key list.\n# @INVARIANT every appended entry includes generated message_id and created_at timestamp.\ndef _append_history(\n user_id: str,\n conversation_id: str,\n role: str,\n text: str,\n state: str | None = None,\n task_id: str | None = None,\n confirmation_id: str | None = None,\n):\n key = (user_id, conversation_id)\n if key not in CONVERSATIONS:\n CONVERSATIONS[key] = []\n CONVERSATIONS[key].append(\n {\n \"message_id\": str(uuid.uuid4()),\n \"conversation_id\": conversation_id,\n \"role\": role,\n \"text\": text,\n \"state\": state,\n \"task_id\": task_id,\n \"confirmation_id\": confirmation_id,\n \"created_at\": datetime.now(UTC),\n }\n )\n\n\n# #endregion _append_history\n" }, { "contract_id": "_persist_message", @@ -5827,7 +5803,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION BINDS_TO -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE payload describes decision/outcome fields.\n# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.utcnow().isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n" + "body": "# #region _audit [C:2] [TYPE Function]\n# @BRIEF Append in-memory audit record for assistant decision trace.\n# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]\n# @RELATION BINDS_TO -> [ASSISTANT_AUDIT]\n# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.\n# @PRE payload describes decision/outcome fields.\n# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.\n# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.\ndef _audit(user_id: str, payload: dict[str, Any]):\n if user_id not in ASSISTANT_AUDIT:\n ASSISTANT_AUDIT[user_id] = []\n ASSISTANT_AUDIT[user_id].append(\n {**payload, \"created_at\": datetime.now(UTC).isoformat()}\n )\n logger.info(f\"[assistant.audit] {payload}\")\n\n\n# #endregion _audit\n" }, { "contract_id": "_persist_audit", @@ -5887,7 +5863,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE confirmation_id references existing row.\n# @POST State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.utcnow()\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n" + "body": "# #region _update_confirmation_state [C:2] [TYPE Function]\n# @BRIEF Update persistent confirmation token lifecycle state.\n# @PRE confirmation_id references existing row.\n# @POST State and consumed_at fields are updated when applicable.\ndef _update_confirmation_state(db: Session, confirmation_id: str, state: str):\n try:\n row = (\n db.query(AssistantConfirmationRecord)\n .filter(AssistantConfirmationRecord.id == confirmation_id)\n .first()\n )\n if not row:\n return\n row.state = state\n if state in {\"consumed\", \"expired\", \"cancelled\"}:\n row.consumed_at = datetime.now(UTC)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(f\"[assistant.confirmation][update_failed] {exc}\")\n\n\n# #endregion _update_confirmation_state\n" }, { "contract_id": "_load_confirmation_from_db", @@ -5954,7 +5930,7 @@ "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", "start_line": 299, - "end_line": 337, + "end_line": 340, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5967,14 +5943,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE db session is available and user_id references current actor scope.\n# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime) and created_at < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n" + "body": "# #region _cleanup_history_ttl [C:2] [TYPE Function]\n# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.\n# @PRE db session is available and user_id references current actor scope.\n# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.\ndef _cleanup_history_ttl(db: Session, user_id: str):\n cutoff = datetime.now() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)\n try:\n query = db.query(AssistantMessageRecord).filter(\n AssistantMessageRecord.user_id == user_id,\n AssistantMessageRecord.created_at < cutoff,\n )\n if hasattr(query, \"delete\"):\n query.delete(synchronize_session=False)\n db.commit()\n except Exception as exc:\n db.rollback()\n logger.warning(\n f\"[assistant.history][ttl_cleanup_failed] user={user_id} error={exc}\"\n )\n\n stale_keys: list[tuple[str, str]] = []\n for key, items in CONVERSATIONS.items():\n if key[0] != user_id:\n continue\n kept = []\n for item in items:\n created_at = item.get(\"created_at\")\n if isinstance(created_at, datetime):\n # DB stores naive datetimes; in-memory items may be aware (from datetime.now(UTC))\n ref = created_at.replace(tzinfo=None) if created_at.tzinfo else created_at\n if ref < cutoff:\n continue\n kept.append(item)\n if kept:\n CONVERSATIONS[key] = kept\n else:\n stale_keys.append(key)\n for key in stale_keys:\n CONVERSATIONS.pop(key, None)\n\n\n# #endregion _cleanup_history_ttl\n" }, { "contract_id": "_is_conversation_archived", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 340, - "end_line": 351, + "start_line": 343, + "end_line": 356, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -5987,14 +5963,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE updated_at can be null for empty conversations.\n# @POST Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n return updated_at < cutoff\n\n\n# #endregion _is_conversation_archived\n" + "body": "# #region _is_conversation_archived [C:2] [TYPE Function]\n# @BRIEF Determine archived state for a conversation based on last update timestamp.\n# @PRE updated_at can be null for empty conversations.\n# @POST Returns True when conversation inactivity exceeds archive threshold.\ndef _is_conversation_archived(updated_at: datetime | None) -> bool:\n if not updated_at:\n return False\n cutoff = datetime.now() - timedelta(days=ASSISTANT_ARCHIVE_AFTER_DAYS)\n # Ensure both operands are naive (DB stores naive datetimes)\n ref = updated_at.replace(tzinfo=None) if updated_at.tzinfo else updated_at\n return ref < cutoff\n\n\n# #endregion _is_conversation_archived\n" }, { "contract_id": "_coerce_query_bool", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_history.py", - "start_line": 354, - "end_line": 366, + "start_line": 359, + "end_line": 371, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6014,7 +5990,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", "start_line": 1, - "end_line": 296, + "end_line": 127, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -6050,81 +6026,34 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]\n# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]\n# @PRE Assistant routes initialized, user authenticated\n# @POST LLM tool catalog filtered and returned\n# @INVARIANT Tool catalog is filtered by user permissions before being sent to LLM.\n# @SIDE_EFFECT Filters tool catalog by user permissions\n# @DATA_CONTRACT UserPermissions -> ToolCatalog\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.dependencies import has_permission\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import resolve_bound_provider_id\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._resolvers import (\n _get_default_environment_id,\n)\nfrom ._schemas import (\n INTENT_PERMISSION_CHECKS,\n)\n\n\n# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE checks list contains resource-action tuples.\n# @POST Returns on first successful permission; raises 403-like HTTPException otherwise.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n\n\n# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Check whether user has at least one permission tuple from the provided list.\n# @PRE current_user and checks list are valid.\n# @POST Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n\n\n# #region _build_tool_catalog [C:3] [TYPE Function]\n# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.\n# @PRE current_user is authenticated; config/db are available.\n# @POST Returns list of executable tools filtered by permission and runtime availability.\n# @RELATION CALLS -> LLMProviderService\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n envs = config_manager.get_environments()\n default_env_id = _get_default_environment_id(config_manager)\n providers = LLMProviderService(db).get_all_providers()\n llm_settings = {}\n try:\n llm_settings = config_manager.get_config().settings.llm\n except Exception:\n llm_settings = {}\n active_provider = next((p.id for p in providers if p.is_active), None)\n fallback_provider = active_provider or (providers[0].id if providers else None)\n validation_provider = (\n resolve_bound_provider_id(llm_settings, \"dashboard_validation\")\n or fallback_provider\n )\n documentation_provider = (\n resolve_bound_provider_id(llm_settings, \"documentation\") or fallback_provider\n )\n\n candidates: list[dict[str, Any]] = [\n {\n \"operation\": \"show_capabilities\",\n \"domain\": \"assistant\",\n \"description\": \"Show available assistant commands and examples\",\n \"required_entities\": [],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_task_status\",\n \"domain\": \"status\",\n \"description\": \"Get task status by task_id or latest user task\",\n \"required_entities\": [],\n \"optional_entities\": [\"task_id\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"create_branch\",\n \"domain\": \"git\",\n \"description\": \"Create git branch for dashboard by id/slug/title\",\n \"required_entities\": [\"branch_name\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"commit_changes\",\n \"domain\": \"git\",\n \"description\": \"Commit dashboard repository changes by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\", \"message\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"deploy_dashboard\",\n \"domain\": \"git\",\n \"description\": \"Deploy dashboard (id/slug/title) to target environment\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"execute_migration\",\n \"domain\": \"migration\",\n \"description\": \"Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters\",\n \"required_entities\": [\"source_env\", \"target_env\"],\n \"optional_entities\": [\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_backup\",\n \"domain\": \"backup\",\n \"description\": \"Run backup for environment or specific dashboard by id/slug/title\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_validation\",\n \"domain\": \"llm\",\n \"description\": \"Run LLM dashboard validation by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_ref\", \"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": validation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_documentation\",\n \"domain\": \"llm\",\n \"description\": \"Generate dataset documentation via LLM\",\n \"required_entities\": [\"dataset_id\"],\n \"optional_entities\": [\"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": documentation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_health_summary\",\n \"domain\": \"health\",\n \"description\": \"Get summary of dashboard health and failing validations\",\n \"required_entities\": [],\n \"optional_entities\": [\"environment\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n ]\n\n available: list[dict[str, Any]] = []\n for tool in candidates:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n\n if dataset_review_context is not None:\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n\n\n# #region _coerce_intent_entities [C:2] [TYPE Function]\n# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.\n# @PRE intent contains entities dict or missing entities.\n# @POST Returned intent has numeric ids coerced where possible and string values stripped.\ndef _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:\n entities = intent.get(\"entities\")\n if not isinstance(entities, dict):\n intent[\"entities\"] = {}\n entities = intent[\"entities\"]\n for key in (\"dashboard_id\", \"dataset_id\"):\n value = entities.get(key)\n if isinstance(value, str) and value.strip().isdigit():\n entities[key] = int(value.strip())\n for key, value in list(entities.items()):\n if isinstance(value, str):\n entities[key] = value.strip()\n return intent\n\n\n# #endregion _coerce_intent_entities\n\n\n# #endregion AssistantLlmPlanner\n" - }, - { - "contract_id": "_check_any_permission", - "contract_type": "Function", - "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 34, - "end_line": 54, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "BRIEF": "Validate user against alternative permission checks (logical OR).", - "COMPLEXITY": 2, - "POST": "Returns on first successful permission; raises 403-like HTTPException otherwise.", - "PRE": "checks list contains resource-action tuples." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE checks list contains resource-action tuples.\n# @POST Returns on first successful permission; raises 403-like HTTPException otherwise.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n" - }, - { - "contract_id": "_has_any_permission", - "contract_type": "Function", - "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 57, - "end_line": 69, - "tier": "TIER_1", - "complexity": 2, - "metadata": { - "BRIEF": "Check whether user has at least one permission tuple from the provided list.", - "COMPLEXITY": 2, - "POST": "Returns True when at least one permission check passes.", - "PRE": "current_user and checks list are valid." - }, - "relations": [], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Check whether user has at least one permission tuple from the provided list.\n# @PRE current_user and checks list are valid.\n# @POST Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n" + "body": "# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]\n# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]\n# @PRE Assistant routes initialized, user authenticated\n# @POST LLM tool catalog filtered and returned\n# @INVARIANT Tool catalog is filtered by user permissions before being sent to LLM.\n# @SIDE_EFFECT Filters tool catalog by user permissions\n# @DATA_CONTRACT UserPermissions -> ToolCatalog\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.schemas.auth import User\n\nfrom ._tool_registry import (\n _check_any_permission,\n _has_any_permission,\n get_catalog,\n)\n\n\n# #region _build_tool_catalog [C:2] [TYPE Function]\n# @BRIEF Build tool catalog from the central registry, appending dataset-review tools when a session is active.\n# @PRE current_user is authenticated; config/db are available.\n# @POST Returns list of executable tools filtered by permission and runtime availability.\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n \"\"\"Build tool catalog from the central registry, with optional dataset-review tools.\"\"\"\n available = get_catalog(current_user, config_manager, db)\n\n # Dataset review tools are session-scoped — only available when a review is active.\n if dataset_review_context is not None:\n from ._schemas import INTENT_PERMISSION_CHECKS\n\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n\n\n# #region _coerce_intent_entities [C:2] [TYPE Function]\n# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.\n# @PRE intent contains entities dict or missing entities.\n# @POST Returned intent has numeric ids coerced where possible and string values stripped.\ndef _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:\n entities = intent.get(\"entities\")\n if not isinstance(entities, dict):\n intent[\"entities\"] = {}\n entities = intent[\"entities\"]\n for key in (\"dashboard_id\", \"dataset_id\"):\n value = entities.get(key)\n if isinstance(value, str) and value.strip().isdigit():\n entities[key] = int(value.strip())\n for key, value in list(entities.items()):\n if isinstance(value, str):\n entities[key] = value.strip()\n return intent\n\n\n# #endregion _coerce_intent_entities\n\n\n# #endregion AssistantLlmPlanner\n" }, { "contract_id": "_build_tool_catalog", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 72, - "end_line": 271, - "tier": "TIER_2", - "complexity": 3, + "start_line": 29, + "end_line": 102, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "BRIEF": "Build current-user tool catalog for LLM planner with operation contracts and defaults.", - "COMPLEXITY": 3, + "BRIEF": "Build tool catalog from the central registry, appending dataset-review tools when a session is active.", + "COMPLEXITY": 2, "POST": "Returns list of executable tools filtered by permission and runtime availability.", "PRE": "current_user is authenticated; config/db are available." }, - "relations": [ - { - "source_id": "_build_tool_catalog", - "relation_type": "CALLS", - "target_id": "LLMProviderService", - "target_ref": "LLMProviderService" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _build_tool_catalog [C:3] [TYPE Function]\n# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.\n# @PRE current_user is authenticated; config/db are available.\n# @POST Returns list of executable tools filtered by permission and runtime availability.\n# @RELATION CALLS -> LLMProviderService\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n envs = config_manager.get_environments()\n default_env_id = _get_default_environment_id(config_manager)\n providers = LLMProviderService(db).get_all_providers()\n llm_settings = {}\n try:\n llm_settings = config_manager.get_config().settings.llm\n except Exception:\n llm_settings = {}\n active_provider = next((p.id for p in providers if p.is_active), None)\n fallback_provider = active_provider or (providers[0].id if providers else None)\n validation_provider = (\n resolve_bound_provider_id(llm_settings, \"dashboard_validation\")\n or fallback_provider\n )\n documentation_provider = (\n resolve_bound_provider_id(llm_settings, \"documentation\") or fallback_provider\n )\n\n candidates: list[dict[str, Any]] = [\n {\n \"operation\": \"show_capabilities\",\n \"domain\": \"assistant\",\n \"description\": \"Show available assistant commands and examples\",\n \"required_entities\": [],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_task_status\",\n \"domain\": \"status\",\n \"description\": \"Get task status by task_id or latest user task\",\n \"required_entities\": [],\n \"optional_entities\": [\"task_id\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"create_branch\",\n \"domain\": \"git\",\n \"description\": \"Create git branch for dashboard by id/slug/title\",\n \"required_entities\": [\"branch_name\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"commit_changes\",\n \"domain\": \"git\",\n \"description\": \"Commit dashboard repository changes by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\", \"message\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"deploy_dashboard\",\n \"domain\": \"git\",\n \"description\": \"Deploy dashboard (id/slug/title) to target environment\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"execute_migration\",\n \"domain\": \"migration\",\n \"description\": \"Run dashboard migration (id/slug/title) between environments. Optional boolean flags: replace_db_config, fix_cross_filters\",\n \"required_entities\": [\"source_env\", \"target_env\"],\n \"optional_entities\": [\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_backup\",\n \"domain\": \"backup\",\n \"description\": \"Run backup for environment or specific dashboard by id/slug/title\",\n \"required_entities\": [\"environment\"],\n \"optional_entities\": [\"dashboard_id\", \"dashboard_ref\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_validation\",\n \"domain\": \"llm\",\n \"description\": \"Run LLM dashboard validation by dashboard id/slug/title\",\n \"required_entities\": [],\n \"optional_entities\": [\"dashboard_ref\", \"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": validation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"run_llm_documentation\",\n \"domain\": \"llm\",\n \"description\": \"Generate dataset documentation via LLM\",\n \"required_entities\": [\"dataset_id\"],\n \"optional_entities\": [\"environment\", \"provider\"],\n \"defaults\": {\n \"environment\": default_env_id,\n \"provider\": documentation_provider,\n },\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"get_health_summary\",\n \"domain\": \"health\",\n \"description\": \"Get summary of dashboard health and failing validations\",\n \"required_entities\": [],\n \"optional_entities\": [\"environment\"],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n ]\n\n available: list[dict[str, Any]] = []\n for tool in candidates:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n\n if dataset_review_context is not None:\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n" + "body": "# #region _build_tool_catalog [C:2] [TYPE Function]\n# @BRIEF Build tool catalog from the central registry, appending dataset-review tools when a session is active.\n# @PRE current_user is authenticated; config/db are available.\n# @POST Returns list of executable tools filtered by permission and runtime availability.\ndef _build_tool_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n dataset_review_context: dict[str, Any] | None = None,\n) -> list[dict[str, Any]]:\n \"\"\"Build tool catalog from the central registry, with optional dataset-review tools.\"\"\"\n available = get_catalog(current_user, config_manager, db)\n\n # Dataset review tools are session-scoped — only available when a review is active.\n if dataset_review_context is not None:\n from ._schemas import INTENT_PERMISSION_CHECKS\n\n dataset_tools: list[dict[str, Any]] = [\n {\n \"operation\": \"dataset_review_answer_context\",\n \"domain\": \"dataset_review\",\n \"description\": \"Answer questions using the currently bound dataset review session context\",\n \"required_entities\": [\"dataset_review_session_id\"],\n \"optional_entities\": [],\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n },\n {\n \"operation\": \"dataset_review_approve_mappings\",\n \"domain\": \"dataset_review\",\n \"description\": \"Approve warning-sensitive execution mappings in the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [\"mapping_ids\"],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_set_field_semantics\",\n \"domain\": \"dataset_review\",\n \"description\": \"Apply explicit semantic field override or candidate selection in the current dataset review session\",\n \"required_entities\": [\n \"dataset_review_session_id\",\n \"session_version\",\n \"field_id\",\n ],\n \"optional_entities\": [\n \"candidate_id\",\n \"verbose_name\",\n \"description\",\n \"display_format\",\n \"lock_field\",\n ],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n {\n \"operation\": \"dataset_review_generate_sql_preview\",\n \"domain\": \"dataset_review\",\n \"description\": \"Generate a Superset-compiled SQL preview for the current dataset review session\",\n \"required_entities\": [\"dataset_review_session_id\", \"session_version\"],\n \"optional_entities\": [],\n \"risk_level\": \"guarded\",\n \"requires_confirmation\": True,\n },\n ]\n for tool in dataset_tools:\n checks = INTENT_PERMISSION_CHECKS.get(tool[\"operation\"], [])\n if checks and not _has_any_permission(current_user, checks):\n continue\n available.append(tool)\n return available\n\n\n# #endregion _build_tool_catalog\n" }, { "contract_id": "_coerce_intent_entities", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner.py", - "start_line": 274, - "end_line": 293, + "start_line": 105, + "end_line": 124, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6144,7 +6073,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", "start_line": 1, - "end_line": 174, + "end_line": 179, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -6174,14 +6103,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]\n# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @PRE Assistant routes initialized, user authenticated\n# @POST Intent planning registered with confirmation gate\n# @INVARIANT Production deployments always require confirmation.\n# @SIDE_EFFECT Registers intent planning routes\n# @DATA_CONTRACT UserIntent -> PlannedAction\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import (\n normalize_llm_settings,\n)\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._llm_planner import (\n _check_any_permission,\n _coerce_intent_entities,\n)\nfrom ._resolvers import (\n _is_production_env,\n _resolve_provider_id,\n)\nfrom ._schemas import INTENT_PERMISSION_CHECKS\n\n\n# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE tools list contains allowed operations for current user.\n# @POST Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a deterministic intent planner for backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n\n\n# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE intent.operation is present for known assistant command domains.\n# @POST Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n if operation in INTENT_PERMISSION_CHECKS:\n _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])\n\n\n# #endregion _authorize_intent\n\n\n# #endregion AssistantLlmPlannerIntent\n" + "body": "# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]\n# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantResolvers]\n# @PRE Assistant routes initialized, user authenticated\n# @POST Intent planning registered with confirmation gate\n# @INVARIANT Production deployments always require confirmation.\n# @SIDE_EFFECT Registers intent planning routes\n# @DATA_CONTRACT UserIntent -> PlannedAction\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import logger\nfrom src.plugins.llm_analysis.models import LLMProviderType\nfrom src.plugins.llm_analysis.service import LLMClient\nfrom src.schemas.auth import User\nfrom src.services.llm_prompt_templates import (\n normalize_llm_settings,\n)\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._llm_planner import (\n _check_any_permission,\n _coerce_intent_entities,\n)\nfrom ._resolvers import (\n _is_production_env,\n _resolve_provider_id,\n)\nfrom ._tool_registry import get_permission_checks\n\n\n# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE tools list contains allowed operations for current user.\n# @POST Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a helpful assistant that understands backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n ' Include \"clarification\" field with a specific question in Russian '\n \"asking what exactly is missing or unclear.\\n\"\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n clarification = response.get(\"clarification\", \"\")\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n \"clarification\": clarification,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n\n\n# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE intent.operation is present for known assistant command domains.\n# @POST Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n checks = get_permission_checks().get(operation)\n if checks:\n _check_any_permission(current_user, checks)\n\n\n# #endregion _authorize_intent\n\n\n# #endregion AssistantLlmPlannerIntent\n" }, { "contract_id": "_plan_intent_with_llm", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", "start_line": 40, - "end_line": 158, + "end_line": 162, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6194,14 +6123,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE tools list contains allowed operations for current user.\n# @POST Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a deterministic intent planner for backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n" + "body": "# #region _plan_intent_with_llm [C:2] [TYPE Function]\n# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.\n# @PRE tools list contains allowed operations for current user.\n# @POST Returns normalized intent dict when planning succeeds; otherwise None.\nasync def _plan_intent_with_llm(\n message: str,\n tools: list[dict[str, Any]],\n db: Session,\n config_manager: ConfigManager,\n) -> dict[str, Any] | None:\n if not tools:\n return None\n\n llm_settings = normalize_llm_settings(config_manager.get_config().settings.llm)\n planner_provider_token = llm_settings.get(\"assistant_planner_provider\")\n planner_model_override = llm_settings.get(\"assistant_planner_model\")\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n provider_id = _resolve_provider_id(planner_provider_token, db)\n provider = next((p for p in providers if p.id == provider_id), None)\n if not provider:\n return None\n api_key = llm_service.get_decrypted_api_key(provider.id)\n if not api_key:\n return None\n\n planner = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=planner_model_override or provider.default_model,\n )\n\n system_instruction = (\n \"You are a helpful assistant that understands backend tools.\\n\"\n \"Choose exactly one operation from available_tools or return clarify.\\n\"\n \"Output strict JSON object:\\n\"\n \"{\"\n '\"domain\": string, '\n '\"operation\": string, '\n '\"entities\": object, '\n '\"confidence\": number, '\n '\"risk_level\": \"safe\"|\"guarded\"|\"dangerous\", '\n '\"requires_confirmation\": boolean'\n \"}\\n\"\n \"Rules:\\n\"\n \"- Use only operation names from available_tools.\\n\"\n '- If input is ambiguous, operation must be \"clarify\" with low confidence.\\n'\n ' Include \"clarification\" field with a specific question in Russian '\n \"asking what exactly is missing or unclear.\\n\"\n \"- If dashboard is provided as name/slug (e.g., COVID), put it into entities.dashboard_ref.\\n\"\n \"- Keep entities minimal and factual.\\n\"\n )\n payload = {\n \"available_tools\": tools,\n \"user_message\": message,\n \"known_environments\": [\n {\"id\": e.id, \"name\": e.name} for e in config_manager.get_environments()\n ],\n }\n try:\n response = await planner.get_json_completion(\n [\n {\"role\": \"system\", \"content\": system_instruction},\n {\"role\": \"user\", \"content\": json.dumps(payload, ensure_ascii=False)},\n ]\n )\n except Exception as exc:\n import traceback\n\n logger.warning(\n f\"[assistant.planner][fallback] LLM planner unavailable: {exc}\\n{traceback.format_exc()}\"\n )\n return None\n if not isinstance(response, dict):\n return None\n\n operation = response.get(\"operation\")\n valid_ops = {tool[\"operation\"] for tool in tools}\n if operation == \"clarify\":\n clarification = response.get(\"clarification\", \"\")\n return {\n \"domain\": \"unknown\",\n \"operation\": \"clarify\",\n \"entities\": {},\n \"confidence\": float(response.get(\"confidence\", 0.3)),\n \"risk_level\": \"safe\",\n \"requires_confirmation\": False,\n \"clarification\": clarification,\n }\n if operation not in valid_ops:\n return None\n\n by_operation = {tool[\"operation\"]: tool for tool in tools}\n selected = by_operation[operation]\n intent = {\n \"domain\": response.get(\"domain\") or selected[\"domain\"],\n \"operation\": operation,\n \"entities\": response.get(\"entities\", {}),\n \"confidence\": float(response.get(\"confidence\", 0.75)),\n \"risk_level\": response.get(\"risk_level\") or selected[\"risk_level\"],\n \"requires_confirmation\": bool(\n response.get(\"requires_confirmation\", selected[\"requires_confirmation\"])\n ),\n }\n intent = _coerce_intent_entities(intent)\n\n defaults = selected.get(\"defaults\") or {}\n for key, value in defaults.items():\n if value and not intent[\"entities\"].get(key):\n intent[\"entities\"][key] = value\n\n if operation in {\"deploy_dashboard\", \"execute_migration\"}:\n env_token = intent[\"entities\"].get(\"environment\") or intent[\"entities\"].get(\n \"target_env\"\n )\n if _is_production_env(env_token, config_manager):\n intent[\"risk_level\"] = \"dangerous\"\n intent[\"requires_confirmation\"] = True\n return intent\n\n\n# #endregion _plan_intent_with_llm\n" }, { "contract_id": "_authorize_intent", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_llm_planner_intent.py", - "start_line": 161, - "end_line": 171, + "start_line": 165, + "end_line": 176, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6214,7 +6143,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE intent.operation is present for known assistant command domains.\n# @POST Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n if operation in INTENT_PERMISSION_CHECKS:\n _check_any_permission(current_user, INTENT_PERMISSION_CHECKS[operation])\n\n\n# #endregion _authorize_intent\n" + "body": "# #region _authorize_intent [C:2] [TYPE Function]\n# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.\n# @PRE intent.operation is present for known assistant command domains.\n# @POST Returns if authorized; raises HTTPException(403) when denied.\ndef _authorize_intent(intent: dict[str, Any], current_user: User):\n operation = intent.get(\"operation\")\n checks = get_permission_checks().get(operation)\n if checks:\n _check_any_permission(current_user, checks)\n\n\n# #endregion _authorize_intent\n" }, { "contract_id": "AssistantResolvers", @@ -6454,7 +6383,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/assistant/_routes.py", "start_line": 1, - "end_line": 319, + "end_line": 326, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -6510,14 +6439,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]\n# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @RELATION DEPENDS_ON -> [AssistantCommandParser]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [AssistantDispatch]\n# @RELATION DISPATCHES -> [AssistantAdminRoutes]\n# @INVARIANT Risky operations are never executed without valid confirmation token.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nimport uuid\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n)\nfrom src.schemas.auth import User\n\nfrom ._command_parser import _parse_command\nfrom ._dataset_review import (\n _load_dataset_review_context,\n _plan_dataset_review_intent,\n)\nfrom ._dispatch import (\n _async_confirmation_summary,\n _clarification_text_for_intent,\n _dispatch_intent,\n)\nfrom ._history import (\n _append_history,\n _audit,\n _load_confirmation_from_db,\n _persist_audit,\n _persist_confirmation,\n _persist_message,\n _resolve_or_create_conversation,\n _update_confirmation_state,\n)\nfrom ._llm_planner import _build_tool_catalog\nfrom ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm\nfrom ._schemas import (\n _SAFE_OPS,\n CONFIRMATIONS,\n AssistantAction,\n AssistantMessageRequest,\n AssistantMessageResponse,\n ConfirmationRecord,\n)\n\nrouter = APIRouter(tags=[\"Assistant\"])\n\n\n@router.post(\"/messages\", response_model=AssistantMessageResponse)\n# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [_dispatch_intent]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE Authenticated user is available and message text is non-empty.\n# @POST Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.\nasync 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)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n 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())\n\n\n# #endregion send_message\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/confirm\", response_model=AssistantMessageResponse\n)\n# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion confirm_operation\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/cancel\", response_model=AssistantMessageResponse\n)\n# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE confirmation_id exists, belongs to current user, and is still pending.\n# @POST Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion cancel_operation\n\n\n# #endregion AssistantRoutes\n" + "body": "# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]\n# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantHistory]\n# @RELATION DEPENDS_ON -> [AssistantCommandParser]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DEPENDS_ON -> [AssistantDatasetReview]\n# @RELATION DEPENDS_ON -> [AssistantDispatch]\n# @RELATION DISPATCHES -> [AssistantAdminRoutes]\n# @INVARIANT Risky operations are never executed without valid confirmation token.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n)\nfrom src.schemas.auth import User\n\nfrom ._command_parser import _parse_command\nfrom ._dataset_review import (\n _load_dataset_review_context,\n _plan_dataset_review_intent,\n)\nfrom ._dispatch import (\n _async_confirmation_summary,\n _clarification_text_for_intent,\n)\nfrom ._tool_registry import dispatch\nfrom ._history import (\n _append_history,\n _audit,\n _load_confirmation_from_db,\n _persist_audit,\n _persist_confirmation,\n _persist_message,\n _resolve_or_create_conversation,\n _update_confirmation_state,\n)\nfrom ._llm_planner import _build_tool_catalog\nfrom ._llm_planner_intent import _authorize_intent, _plan_intent_with_llm\nfrom ._tool_registry import get_safe_ops\nfrom ._schemas import (\n CONFIRMATIONS,\n AssistantAction,\n AssistantMessageRequest,\n AssistantMessageResponse,\n ConfirmationRecord,\n)\n\nrouter = APIRouter(tags=[\"Assistant\"])\n\n\n@router.post(\"/messages\", response_model=AssistantMessageResponse)\n# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [dispatch]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE Authenticated user is available and message text is non-empty.\n# @POST Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.\nasync 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)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n # Use LLM-generated clarification question when available (from _plan_intent_with_llm),\n # otherwise fall back to deterministic text.\n text = (\n intent.get('clarification')\n or 'Команда неоднозначна. Уточните, что вы хотите сделать.'\n )\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.now(UTC))\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in get_safe_ops():\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.now(UTC) + __import__('datetime').timedelta(minutes=5), created_at=datetime.now(UTC))\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.now(UTC))\n text, task_id, actions = await dispatch(intent.get('operation'), intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.now(UTC))\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n 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.now(UTC))\n\n\n# #endregion send_message\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/confirm\", response_model=AssistantMessageResponse\n)\n# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.now(UTC) > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await dispatch(\n intent.get('operation'), intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.now(UTC),\n )\n\n\n\n# #endregion confirm_operation\n\n\n@router.post(\n \"/confirmations/{confirmation_id}/cancel\", response_model=AssistantMessageResponse\n)\n# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE confirmation_id exists, belongs to current user, and is still pending.\n# @POST Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.now(UTC),\n )\n\n\n\n# #endregion cancel_operation\n\n\n# #endregion AssistantRoutes\n" }, { "contract_id": "send_message", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", "start_line": 67, - "end_line": 154, + "end_line": 159, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -6545,8 +6474,8 @@ { "source_id": "send_message", "relation_type": "DEPENDS_ON", - "target_id": "_dispatch_intent", - "target_ref": "[_dispatch_intent]" + "target_id": "dispatch", + "target_ref": "[dispatch]" }, { "source_id": "send_message", @@ -6570,14 +6499,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [_dispatch_intent]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE Authenticated user is available and message text is non-empty.\n# @POST Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.\nasync 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)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n text = 'Команда неоднозначна. Уточните действие: git / migration / backup / llm / status.'\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.utcnow())\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in _SAFE_OPS:\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.utcnow() + __import__('datetime').timedelta(minutes=5), created_at=datetime.utcnow())\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.utcnow())\n text, task_id, actions = await _dispatch_intent(intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.utcnow())\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n 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())\n\n\n# #endregion send_message\n" + "body": "# #region send_message [C:5] [TYPE Function]\n# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.\n# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]\n# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]\n# @RELATION DEPENDS_ON -> [_parse_command]\n# @RELATION DEPENDS_ON -> [dispatch]\n# @RELATION DEPENDS_ON -> [_append_history]\n# @RELATION DEPENDS_ON -> [_persist_message]\n# @RELATION DEPENDS_ON -> [_audit]\n# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.\n# @PRE Authenticated user is available and message text is non-empty.\n# @POST Response state is one of clarification/confirmation/started/success/denied/failed.\n# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.\nasync 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)):\n with belief_scope('send_message'):\n logger.reason('Belief protocol reasoning checkpoint for send_message')\n user_id = current_user.id\n dataset_review_context = _load_dataset_review_context(request.dataset_review_session_id, current_user, db)\n conversation_id = _resolve_or_create_conversation(user_id, request.conversation_id, db)\n _append_history(user_id, conversation_id, 'user', request.message)\n _persist_message(db, user_id, conversation_id, 'user', request.message)\n tools_catalog = _build_tool_catalog(current_user, config_manager, db)\n intent = None\n try:\n intent = await _plan_intent_with_llm(request.message, tools_catalog, db, config_manager)\n except Exception as exc:\n logger.warning(f'[assistant.planner][fallback] Planner error: {exc}')\n if not intent:\n intent = _parse_command(request.message, config_manager)\n if dataset_review_context:\n dataset_review_intent = _plan_dataset_review_intent(request.message, dataset_review_context)\n if dataset_review_intent is not None:\n intent = dataset_review_intent\n confidence = float(intent.get('confidence', 0.0))\n if intent.get('domain') == 'unknown' or confidence < 0.6:\n # Use LLM-generated clarification question when available (from _plan_intent_with_llm),\n # otherwise fall back to deterministic text.\n text = (\n intent.get('clarification')\n or 'Команда неоднозначна. Уточните, что вы хотите сделать.'\n )\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_clarification')\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_clarification', metadata={'intent': intent})\n audit_payload = {'decision': 'needs_clarification', 'message': request.message, 'intent': intent, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_clarification', text=text, intent=intent, actions=[AssistantAction(type='rephrase', label='Rephrase command')], created_at=datetime.now(UTC))\n try:\n _authorize_intent(intent, current_user)\n operation = intent.get('operation')\n if operation not in get_safe_ops():\n confirmation_id = str(uuid.uuid4())\n confirm = ConfirmationRecord(id=confirmation_id, user_id=user_id, conversation_id=conversation_id, intent=intent, dispatch={'intent': intent}, expires_at=datetime.now(UTC) + __import__('datetime').timedelta(minutes=5), created_at=datetime.now(UTC))\n CONFIRMATIONS[confirmation_id] = confirm\n _persist_confirmation(db, confirm)\n text = await _async_confirmation_summary(intent, config_manager, db)\n _append_history(user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state='needs_confirmation', confirmation_id=confirmation_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [{'type': 'confirm', 'label': '✅ Подтвердить', 'target': confirmation_id}, {'type': 'cancel', 'label': '❌ Отменить', 'target': confirmation_id}]})\n audit_payload = {'decision': 'needs_confirmation', 'message': request.message, 'intent': intent, 'confirmation_id': confirmation_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state='needs_confirmation', text=text, intent=intent, confirmation_id=confirmation_id, actions=[AssistantAction(type='confirm', label='✅ Подтвердить', target=confirmation_id), AssistantAction(type='cancel', label='❌ Отменить', target=confirmation_id)], created_at=datetime.now(UTC))\n text, task_id, actions = await dispatch(intent.get('operation'), intent, current_user, task_manager, config_manager, db)\n state = 'started' if task_id else 'success'\n _append_history(user_id, conversation_id, 'assistant', text, state=state, task_id=task_id)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, task_id=task_id, metadata={'intent': intent, 'dataset_review_context': dataset_review_context, 'actions': [a.model_dump() for a in actions]})\n audit_payload = {'decision': 'executed', 'message': request.message, 'intent': intent, 'task_id': task_id, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n return AssistantMessageResponse(conversation_id=conversation_id, response_id=str(uuid.uuid4()), state=state, text=text, intent=intent, task_id=task_id, actions=actions, created_at=datetime.now(UTC))\n except HTTPException as exc:\n detail_text = str(exc.detail)\n is_clarification_error = exc.status_code in (400, 422) and (detail_text.lower().startswith('missing') or 'укажите' in detail_text.lower() or 'выберите' in detail_text.lower())\n if exc.status_code == status.HTTP_403_FORBIDDEN:\n state = 'denied'\n elif is_clarification_error:\n state = 'needs_clarification'\n else:\n state = 'failed'\n text = _clarification_text_for_intent(intent, detail_text) if state == 'needs_clarification' else detail_text\n _append_history(user_id, conversation_id, 'assistant', text, state=state)\n _persist_message(db, user_id, conversation_id, 'assistant', text, state=state, metadata={'intent': intent})\n audit_payload = {'decision': state, 'message': request.message, 'intent': intent, 'error': text, 'dataset_review_session_id': request.dataset_review_session_id}\n _audit(user_id, audit_payload)\n _persist_audit(db, user_id, audit_payload, conversation_id)\n logger.reflect('Belief protocol postcondition checkpoint for send_message')\n 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.now(UTC))\n\n\n# #endregion send_message\n" }, { "contract_id": "confirm_operation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", - "start_line": 160, - "end_line": 241, + "start_line": 165, + "end_line": 247, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6590,14 +6519,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.utcnow() > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await _dispatch_intent(\n intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion confirm_operation\n" + "body": "# #region confirm_operation [C:2] [TYPE Function]\n# @BRIEF Execute previously requested risky operation after explicit user confirmation.\n# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.\n# @POST Confirmation state becomes consumed and operation result is persisted in history.\nasync def confirm_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n task_manager: TaskManager = Depends(get_task_manager),\n config_manager: ConfigManager = Depends(get_config_manager),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.confirm\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n if datetime.now(UTC) > record.expires_at:\n record.state = \"expired\"\n _update_confirmation_state(db, confirmation_id, \"expired\")\n raise HTTPException(status_code=400, detail=\"Confirmation expired\")\n\n intent = record.intent\n text, task_id, actions = await dispatch(\n intent.get('operation'), intent, current_user, task_manager, config_manager, db\n )\n record.state = \"consumed\"\n _update_confirmation_state(db, confirmation_id, \"consumed\")\n\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"started\" if task_id else \"success\",\n task_id=task_id,\n metadata={\"intent\": intent, \"confirmation_id\": confirmation_id},\n )\n audit_payload = {\n \"decision\": \"confirmed_execute\",\n \"confirmation_id\": confirmation_id,\n \"task_id\": task_id,\n \"intent\": intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"started\" if task_id else \"success\",\n text=text,\n intent=intent,\n task_id=task_id,\n actions=actions,\n created_at=datetime.now(UTC),\n )\n\n\n\n# #endregion confirm_operation\n" }, { "contract_id": "cancel_operation", "contract_type": "Function", "file_path": "backend/src/api/routes/assistant/_routes.py", - "start_line": 247, - "end_line": 316, + "start_line": 253, + "end_line": 323, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -6610,7 +6539,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE confirmation_id exists, belongs to current user, and is still pending.\n# @POST Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.utcnow(),\n )\n\n\n# #endregion cancel_operation\n" + "body": "# #region cancel_operation [C:2] [TYPE Function]\n# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.\n# @PRE confirmation_id exists, belongs to current user, and is still pending.\n# @POST Confirmation becomes cancelled and cannot be executed anymore.\nasync def cancel_operation(\n confirmation_id: str,\n current_user: User = Depends(get_current_user),\n db: Session = Depends(get_db),\n):\n with belief_scope(\"assistant.cancel\"):\n record = CONFIRMATIONS.get(confirmation_id)\n if not record:\n record = _load_confirmation_from_db(db, confirmation_id)\n if record:\n CONFIRMATIONS[confirmation_id] = record\n else:\n raise HTTPException(status_code=404, detail=\"Confirmation not found\")\n\n if record.user_id != current_user.id:\n raise HTTPException(\n status_code=403, detail=\"Confirmation does not belong to current user\"\n )\n\n if record.state != \"pending\":\n raise HTTPException(\n status_code=400, detail=f\"Confirmation already {record.state}\"\n )\n\n record.state = \"cancelled\"\n _update_confirmation_state(db, confirmation_id, \"cancelled\")\n text = \"Операция отменена. Выполнение не запускалось.\"\n _append_history(\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n )\n _persist_message(\n db,\n current_user.id,\n record.conversation_id,\n \"assistant\",\n text,\n state=\"success\",\n confirmation_id=confirmation_id,\n metadata={\"intent\": record.intent},\n )\n audit_payload = {\n \"decision\": \"cancelled\",\n \"confirmation_id\": confirmation_id,\n \"intent\": record.intent,\n }\n _audit(current_user.id, audit_payload)\n _persist_audit(db, current_user.id, audit_payload, record.conversation_id)\n\n return AssistantMessageResponse(\n conversation_id=record.conversation_id,\n response_id=str(uuid.uuid4()),\n state=\"success\",\n text=text,\n intent=record.intent,\n confirmation_id=confirmation_id,\n actions=[],\n created_at=datetime.now(UTC),\n )\n\n\n\n# #endregion cancel_operation\n" }, { "contract_id": "AssistantSchemas", @@ -6826,6 +6755,926 @@ "tier_source": "AutoCalculated", "body": "# #region ASSISTANT_AUDIT [C:1] [TYPE Constant]\n# @BRIEF In-memory assistant audit trail: {user_id -> [audit_records]}. Volatile — lost on restart.\n# #endregion ASSISTANT_AUDIT\n" }, + { + "contract_id": "AssistantToolBackup", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_backup.py", + "start_line": 1, + "end_line": 100, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"run_backup\" tool — run backup for environment or specific dashboard.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolBackup", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolBackup", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolBackup [C:3] [TYPE Module] [SEMANTICS assistant, tool, backup]\n# @BRIEF Handler for the \"run_backup\" tool — run backup for environment or specific dashboard.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import (\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n)\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_run_backup [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_backup\",\n domain=\"backup\",\n description=\"Run backup for environment or specific dashboard by id/slug/title\",\n required_entities=[\"environment\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[\n (\"plugin:superset-backup\", \"EXECUTE\"),\n (\"plugin:backup\", \"EXECUTE\"),\n ],\n)\n@belief_scope(\"run_backup\")\nasync def handle_run_backup(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run backup for environment or specific dashboard.\"\"\"\n _check_any_permission(\n current_user,\n [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n )\n entities = intent.get(\"entities\", {})\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(\n entities, config_manager, env_hint=env_token\n )\n if not dashboard_id:\n raise HTTPException(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref\"\n )\n params[\"dashboard_ids\"] = [dashboard_id]\n task = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=params, user_id=current_user.id\n )\n actions: list[AssistantAction] = [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ]\n if entities.get(\"dashboard_id\") or entities.get(\"dashboard_ref\"):\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n if dashboard_id:\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n actions.append(\n AssistantAction(\n type=\"open_diff\", label=\"Показать Diff\", target=str(dashboard_id)\n )\n )\n return (f\"Бэкап запущен. task_id={task.id}\", task.id, actions)\n\n\n# #endregion handle_run_backup\n# #endregion AssistantToolBackup\n" + }, + { + "contract_id": "handle_run_backup", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_backup.py", + "start_line": 28, + "end_line": 99, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_run_backup [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_backup\",\n domain=\"backup\",\n description=\"Run backup for environment or specific dashboard by id/slug/title\",\n required_entities=[\"environment\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[\n (\"plugin:superset-backup\", \"EXECUTE\"),\n (\"plugin:backup\", \"EXECUTE\"),\n ],\n)\n@belief_scope(\"run_backup\")\nasync def handle_run_backup(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run backup for environment or specific dashboard.\"\"\"\n _check_any_permission(\n current_user,\n [(\"plugin:superset-backup\", \"EXECUTE\"), (\"plugin:backup\", \"EXECUTE\")],\n )\n entities = intent.get(\"entities\", {})\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(\n entities, config_manager, env_hint=env_token\n )\n if not dashboard_id:\n raise HTTPException(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref\"\n )\n params[\"dashboard_ids\"] = [dashboard_id]\n task = await task_manager.create_task(\n plugin_id=\"superset-backup\", params=params, user_id=current_user.id\n )\n actions: list[AssistantAction] = [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ]\n if entities.get(\"dashboard_id\") or entities.get(\"dashboard_ref\"):\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n if dashboard_id:\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {_get_environment_name_by_id(env_id, config_manager)}\",\n target=f\"/dashboards/{dashboard_id}?env_id={env_id}\",\n )\n )\n actions.append(\n AssistantAction(\n type=\"open_diff\", label=\"Показать Diff\", target=str(dashboard_id)\n )\n )\n return (f\"Бэкап запущен. task_id={task.id}\", task.id, actions)\n\n\n# #endregion handle_run_backup\n" + }, + { + "contract_id": "AssistantToolCapabilities", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_capabilities.py", + "start_line": 1, + "end_line": 67, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"show_capabilities\" tool — lists available assistant commands and examples.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolCapabilities", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolCapabilities [C:3] [TYPE Module] [SEMANTICS assistant, tool, capabilities, catalog]\n# @BRIEF Handler for the \"show_capabilities\" tool — lists available assistant commands and examples.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import assistant_tool, get_catalog\n\n_HUMAN_LABELS: dict[str, str] = {\n \"create_branch\": \"Git: создание ветки\",\n \"commit_changes\": \"Git: коммит\",\n \"deploy_dashboard\": \"Git: деплой дашборда\",\n \"execute_migration\": \"Миграции: запуск переноса\",\n \"run_backup\": \"Бэкапы: запуск резервного копирования\",\n \"run_llm_validation\": \"LLM: валидация дашборда\",\n \"run_llm_documentation\": \"LLM: генерация документации\",\n \"get_task_status\": \"Статус: проверка задачи\",\n \"get_health_summary\": \"Здоровье: сводка по дашбордам\",\n}\n\n\n# #region handle_show_capabilities [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"show_capabilities\",\n domain=\"assistant\",\n description=\"Show available assistant commands and examples\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"show_capabilities\")\nasync def handle_show_capabilities(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Show available assistant commands and examples.\"\"\"\n tools_catalog = get_catalog(current_user, config_manager, db)\n available = [\n _HUMAN_LABELS[t[\"operation\"]]\n for t in tools_catalog\n if t[\"operation\"] in _HUMAN_LABELS\n ]\n if not available:\n return (\"Сейчас нет доступных для вас операций ассистента.\", None, [])\n commands = \"\\n\".join(f\"- {item}\" for item in available)\n text = (\n f\"Вот что я могу сделать для вас:\\n{commands}\\n\\n\"\n \"Пример: `запусти миграцию с dev на prod для дашборда 42`.\"\n )\n return (text, None, [])\n\n\n# #endregion handle_show_capabilities\n# #endregion AssistantToolCapabilities\n" + }, + { + "contract_id": "handle_show_capabilities", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_capabilities.py", + "start_line": 33, + "end_line": 66, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_show_capabilities [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"show_capabilities\",\n domain=\"assistant\",\n description=\"Show available assistant commands and examples\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"show_capabilities\")\nasync def handle_show_capabilities(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Show available assistant commands and examples.\"\"\"\n tools_catalog = get_catalog(current_user, config_manager, db)\n available = [\n _HUMAN_LABELS[t[\"operation\"]]\n for t in tools_catalog\n if t[\"operation\"] in _HUMAN_LABELS\n ]\n if not available:\n return (\"Сейчас нет доступных для вас операций ассистента.\", None, [])\n commands = \"\\n\".join(f\"- {item}\" for item in available)\n text = (\n f\"Вот что я могу сделать для вас:\\n{commands}\\n\\n\"\n \"Пример: `запусти миграцию с dev на prod для дашборда 42`.\"\n )\n return (text, None, [])\n\n\n# #endregion handle_show_capabilities\n" + }, + { + "contract_id": "AssistantToolCommit", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_commit.py", + "start_line": 1, + "end_line": 56, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"commit_changes\" tool — commit dashboard repository changes.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolCommit", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolCommit", + "relation_type": "DEPENDS_ON", + "target_id": "GitService", + "target_ref": "[GitService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolCommit [C:3] [TYPE Module] [SEMANTICS assistant, tool, git, commit]\n# @BRIEF Handler for the \"commit_changes\" tool — commit dashboard repository changes.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [GitService]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _resolve_dashboard_id_entity\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\nfrom ._dispatch import _get_git_service\n\n\n# #region handle_commit_changes [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"commit_changes\",\n domain=\"git\",\n description=\"Commit dashboard repository changes by dashboard id/slug/title\",\n optional_entities=[\"dashboard_id\", \"dashboard_ref\", \"message\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"commit_changes\")\nasync def handle_commit_changes(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Commit dashboard repository changes.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\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 _get_git_service().commit_changes(dashboard_id, commit_message, None)\n return (\"Коммит выполнен успешно.\", None, [])\n\n\n# #endregion handle_commit_changes\n# #endregion AssistantToolCommit\n" + }, + { + "contract_id": "handle_commit_changes", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_commit.py", + "start_line": 26, + "end_line": 55, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_commit_changes [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"commit_changes\",\n domain=\"git\",\n description=\"Commit dashboard repository changes by dashboard id/slug/title\",\n optional_entities=[\"dashboard_id\", \"dashboard_ref\", \"message\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"commit_changes\")\nasync def handle_commit_changes(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Commit dashboard repository changes.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\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 _get_git_service().commit_changes(dashboard_id, commit_message, None)\n return (\"Коммит выполнен успешно.\", None, [])\n\n\n# #endregion handle_commit_changes\n" + }, + { + "contract_id": "AssistantToolCreateBranch", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_create_branch.py", + "start_line": 1, + "end_line": 59, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"create_branch\" tool — create git branch for a dashboard.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolCreateBranch", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolCreateBranch", + "relation_type": "DEPENDS_ON", + "target_id": "GitService", + "target_ref": "[GitService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolCreateBranch [C:3] [TYPE Module] [SEMANTICS assistant, tool, git, branch]\n# @BRIEF Handler for the \"create_branch\" tool — create git branch for a dashboard.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [GitService]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _resolve_dashboard_id_entity\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\nfrom ._dispatch import _get_git_service\n\n\n# #region handle_create_branch [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"create_branch\",\n domain=\"git\",\n description=\"Create git branch for dashboard by id/slug/title\",\n required_entities=[\"branch_name\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"create_branch\")\nasync def handle_create_branch(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Create git branch for dashboard by id/slug/title.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\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(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref or branch_name\"\n )\n _get_git_service().create_branch(dashboard_id, branch_name, \"main\")\n return (f\"Ветка `{branch_name}` создана для дашборда {dashboard_id}.\", None, [])\n\n\n# #endregion handle_create_branch\n# #endregion AssistantToolCreateBranch\n" + }, + { + "contract_id": "handle_create_branch", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_create_branch.py", + "start_line": 26, + "end_line": 58, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_create_branch [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"create_branch\",\n domain=\"git\",\n description=\"Create git branch for dashboard by id/slug/title\",\n required_entities=[\"branch_name\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"create_branch\")\nasync def handle_create_branch(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Create git branch for dashboard by id/slug/title.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\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(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref or branch_name\"\n )\n _get_git_service().create_branch(dashboard_id, branch_name, \"main\")\n return (f\"Ветка `{branch_name}` создана для дашборда {dashboard_id}.\", None, [])\n\n\n# #endregion handle_create_branch\n" + }, + { + "contract_id": "AssistantToolDeploy", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_deploy.py", + "start_line": 1, + "end_line": 77, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"deploy_dashboard\" tool — deploy dashboard to target environment.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolDeploy", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolDeploy", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolDeploy [C:3] [TYPE Module] [SEMANTICS assistant, tool, git, deploy]\n# @BRIEF Handler for the \"deploy_dashboard\" tool — deploy dashboard to target environment.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _resolve_dashboard_id_entity, _resolve_env_id\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_deploy_dashboard [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"deploy_dashboard\",\n domain=\"git\",\n description=\"Deploy dashboard (id/slug/title) to target environment\",\n required_entities=[\"environment\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"deploy_dashboard\")\nasync def handle_deploy_dashboard(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Deploy dashboard to target environment.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n if not dashboard_id or not env_id:\n raise HTTPException(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref or environment\"\n )\n task = await task_manager.create_task(\n plugin_id=\"git-integration\",\n params={\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": env_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"Деплой запущен. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_deploy_dashboard\n# #endregion AssistantToolDeploy\n" + }, + { + "contract_id": "handle_deploy_dashboard", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_deploy.py", + "start_line": 24, + "end_line": 76, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_deploy_dashboard [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"deploy_dashboard\",\n domain=\"git\",\n description=\"Deploy dashboard (id/slug/title) to target environment\",\n required_entities=[\"environment\"],\n optional_entities=[\"dashboard_id\", \"dashboard_ref\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:git\", \"EXECUTE\")],\n)\n@belief_scope(\"deploy_dashboard\")\nasync def handle_deploy_dashboard(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Deploy dashboard to target environment.\"\"\"\n _check_any_permission(current_user, [(\"plugin:git\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n env_id = _resolve_env_id(env_token, config_manager)\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n if not dashboard_id or not env_id:\n raise HTTPException(\n status_code=422, detail=\"Missing dashboard_id/dashboard_ref or environment\"\n )\n task = await task_manager.create_task(\n plugin_id=\"git-integration\",\n params={\n \"operation\": \"deploy\",\n \"dashboard_id\": dashboard_id,\n \"environment_id\": env_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"Деплой запущен. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_deploy_dashboard\n" + }, + { + "contract_id": "AssistantToolHealthSummary", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_health_summary.py", + "start_line": 1, + "end_line": 81, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"get_health_summary\" tool — get summary of dashboard health.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolHealthSummary", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolHealthSummary", + "relation_type": "DEPENDS_ON", + "target_id": "HealthService", + "target_ref": "[HealthService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolHealthSummary [C:3] [TYPE Module] [SEMANTICS assistant, tool, health, summary]\n# @BRIEF Handler for the \"get_health_summary\" tool — get summary of dashboard health.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [HealthService]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.health_service import HealthService\n\nfrom ._resolvers import _get_environment_name_by_id, _resolve_env_id\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import assistant_tool\n\n\n# #region handle_get_health_summary [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"get_health_summary\",\n domain=\"health\",\n description=\"Get summary of dashboard health and failing validations\",\n optional_entities=[\"environment\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:migration\", \"READ\")],\n)\n@belief_scope(\"get_health_summary\")\nasync def handle_get_health_summary(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Get summary of dashboard health and failing validations.\"\"\"\n entities = intent.get(\"entities\", {})\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 = (\n _get_environment_name_by_id(env_id, config_manager)\n if env_id\n else \"всех окружений\"\n )\n text = (\n f\"Сводка здоровья дашбордов для {env_name}:\\n\"\n f\"- ✅ Прошли проверку: {summary.pass_count}\\n\"\n f\"- ⚠️ С предупреждениями: {summary.warn_count}\\n\"\n f\"- ❌ Ошибки валидации: {summary.fail_count}\\n\"\n f\"- ❓ Неизвестно: {summary.unknown_count}\"\n )\n actions: list[AssistantAction] = [\n AssistantAction(\n type=\"open_route\", label=\"Открыть Health Center\", target=\"/dashboards/health\"\n )\n ]\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(\n AssistantAction(\n type=\"open_route\",\n label=f\"Отчет {item.dashboard_id}\",\n target=f\"/reports/llm/{item.task_id}\",\n )\n )\n return (text, None, actions[:5])\n\n\n# #endregion handle_get_health_summary\n# #endregion AssistantToolHealthSummary\n" + }, + { + "contract_id": "handle_get_health_summary", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_health_summary.py", + "start_line": 24, + "end_line": 80, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_get_health_summary [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"get_health_summary\",\n domain=\"health\",\n description=\"Get summary of dashboard health and failing validations\",\n optional_entities=[\"environment\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:migration\", \"READ\")],\n)\n@belief_scope(\"get_health_summary\")\nasync def handle_get_health_summary(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Get summary of dashboard health and failing validations.\"\"\"\n entities = intent.get(\"entities\", {})\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 = (\n _get_environment_name_by_id(env_id, config_manager)\n if env_id\n else \"всех окружений\"\n )\n text = (\n f\"Сводка здоровья дашбордов для {env_name}:\\n\"\n f\"- ✅ Прошли проверку: {summary.pass_count}\\n\"\n f\"- ⚠️ С предупреждениями: {summary.warn_count}\\n\"\n f\"- ❌ Ошибки валидации: {summary.fail_count}\\n\"\n f\"- ❓ Неизвестно: {summary.unknown_count}\"\n )\n actions: list[AssistantAction] = [\n AssistantAction(\n type=\"open_route\", label=\"Открыть Health Center\", target=\"/dashboards/health\"\n )\n ]\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(\n AssistantAction(\n type=\"open_route\",\n label=f\"Отчет {item.dashboard_id}\",\n target=f\"/reports/llm/{item.task_id}\",\n )\n )\n return (text, None, actions[:5])\n\n\n# #endregion handle_get_health_summary\n" + }, + { + "contract_id": "AssistantToolListEnvironments", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_list_environments.py", + "start_line": 1, + "end_line": 65, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Handler for the \"list_environments\" tool — show available Superset environments.", + "COMPLEXITY": 2, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolListEnvironments", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolListEnvironments [C:2] [TYPE Module] [SEMANTICS assistant, tool, environments, list]\n# @BRIEF Handler for the \"list_environments\" tool — show available Superset environments.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import assistant_tool\n\n\n# #region handle_list_environments [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_environments\",\n domain=\"environments\",\n description=\"Show available Superset environments with their id, name, and stage (DEV/PREPROD/PROD)\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"list_environments\")\nasync def handle_list_environments(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Show available Superset environments.\"\"\"\n envs = config_manager.get_environments()\n\n if not envs:\n return (\"Нет доступных окружений.\", None, [])\n\n lines = [\"**Доступные окружения:**\"]\n for env in envs:\n stage_icon = {\"DEV\": \"🟢\", \"PREPROD\": \"🟡\", \"PROD\": \"🔴\"}\n icon = stage_icon.get(env.stage, \"⚪\")\n prod_tag = \" ⚠️ PRODUCTION\" if env.is_production else \"\"\n default_tag = \" ★ По умолчанию\" if env.is_default else \"\"\n lines.append(\n f\"{icon} **{env.id}** — {env.name}{prod_tag}{default_tag}\"\n )\n\n actions = [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть настройки окружений\",\n target=\"/environments\",\n )\n ]\n\n return (\"\\n\".join(lines), None, actions)\n\n\n# #endregion handle_list_environments\n# #endregion AssistantToolListEnvironments\n" + }, + { + "contract_id": "handle_list_environments", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_list_environments.py", + "start_line": 21, + "end_line": 64, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_list_environments [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_environments\",\n domain=\"environments\",\n description=\"Show available Superset environments with their id, name, and stage (DEV/PREPROD/PROD)\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"list_environments\")\nasync def handle_list_environments(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Show available Superset environments.\"\"\"\n envs = config_manager.get_environments()\n\n if not envs:\n return (\"Нет доступных окружений.\", None, [])\n\n lines = [\"**Доступные окружения:**\"]\n for env in envs:\n stage_icon = {\"DEV\": \"🟢\", \"PREPROD\": \"🟡\", \"PROD\": \"🔴\"}\n icon = stage_icon.get(env.stage, \"⚪\")\n prod_tag = \" ⚠️ PRODUCTION\" if env.is_production else \"\"\n default_tag = \" ★ По умолчанию\" if env.is_default else \"\"\n lines.append(\n f\"{icon} **{env.id}** — {env.name}{prod_tag}{default_tag}\"\n )\n\n actions = [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть настройки окружений\",\n target=\"/environments\",\n )\n ]\n\n return (\"\\n\".join(lines), None, actions)\n\n\n# #endregion handle_list_environments\n" + }, + { + "contract_id": "AssistantToolLlm", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_llm.py", + "start_line": 1, + "end_line": 127, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Handlers for LLM operations — list providers and check LLM status.", + "COMPLEXITY": 2, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolLlm", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolLlm", + "relation_type": "DEPENDS_ON", + "target_id": "LLMProviderService", + "target_ref": "[LLMProviderService]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolLlm [C:2] [TYPE Module] [SEMANTICS assistant, tool, llm, providers, status]\n# @BRIEF Handlers for LLM operations — list providers and check LLM status.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import assistant_tool\n\n\n# #region handle_list_llm_providers [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_llm_providers\",\n domain=\"llm\",\n description=\"List configured LLM providers with their type, model, and active status\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"list_llm_providers\")\nasync def handle_list_llm_providers(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"List configured LLM providers.\"\"\"\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n\n if not providers:\n return (\"LLM провайдеры не настроены.\", None, [])\n\n lines = [\"**🤖 LLM провайдеры:**\"]\n for p in providers:\n icon = \"🟢\" if p.is_active else \"⚪\"\n model = p.default_model or \"модель не указана\"\n lines.append(\n f\"{icon} **{p.name}** — {p.provider_type}\"\n f\"\\n └ Модель: {model}\"\n )\n\n return (\n \"\\n\".join(lines),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Настройки LLM\",\n target=\"/settings/llm\",\n )\n ],\n )\n\n\n# #endregion handle_list_llm_providers\n\n\n# #region handle_get_llm_status [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"get_llm_status\",\n domain=\"llm\",\n description=\"Check if LLM is configured and ready for dashboard validation and documentation\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"get_llm_status\")\nasync def handle_get_llm_status(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Check LLM configuration and readiness status.\"\"\"\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 lines = [\"**🤖 LLM Статус:**\"]\n\n if not providers:\n lines.append(\"❌ Провайдеры не настроены.\")\n return (\"\\n\".join(lines), None, [])\n\n lines.append(f\"📊 Всего провайдеров: {len(providers)}\")\n lines.append(f\"🟢 Активных: {len([p for p in providers if p.is_active])}\")\n lines.append(f\"⚪ Неактивных: {len([p for p in providers if not p.is_active])}\")\n\n if active_provider:\n api_key = service.get_decrypted_api_key(active_provider.id)\n from src.api.routes.llm import _is_valid_runtime_api_key\n\n key_valid = _is_valid_runtime_api_key(api_key)\n lines.append(f\"\\n**Активный провайдер:** {active_provider.name}\")\n lines.append(f\" └ Тип: {active_provider.provider_type}\")\n lines.append(f\" └ Модель: {active_provider.default_model or 'не указана'}\")\n lines.append(f\" └ API ключ: {'✅ валидный' if key_valid else '❌ невалидный/отсутствует'}\")\n lines.append(f\" └ Статус: {'✅ готов' if key_valid else '❌ настроен не полностью'}\")\n\n return (\n \"\\n\".join(lines),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Настройки LLM\",\n target=\"/settings/llm\",\n )\n ],\n )\n\n\n# #endregion handle_get_llm_status\n# #endregion AssistantToolLlm\n" + }, + { + "contract_id": "handle_list_llm_providers", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_llm.py", + "start_line": 23, + "end_line": 68, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_list_llm_providers [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_llm_providers\",\n domain=\"llm\",\n description=\"List configured LLM providers with their type, model, and active status\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"list_llm_providers\")\nasync def handle_list_llm_providers(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"List configured LLM providers.\"\"\"\n service = LLMProviderService(db)\n providers = service.get_all_providers()\n\n if not providers:\n return (\"LLM провайдеры не настроены.\", None, [])\n\n lines = [\"**🤖 LLM провайдеры:**\"]\n for p in providers:\n icon = \"🟢\" if p.is_active else \"⚪\"\n model = p.default_model or \"модель не указана\"\n lines.append(\n f\"{icon} **{p.name}** — {p.provider_type}\"\n f\"\\n └ Модель: {model}\"\n )\n\n return (\n \"\\n\".join(lines),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Настройки LLM\",\n target=\"/settings/llm\",\n )\n ],\n )\n\n\n# #endregion handle_list_llm_providers\n" + }, + { + "contract_id": "handle_get_llm_status", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_llm.py", + "start_line": 71, + "end_line": 126, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_get_llm_status [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"get_llm_status\",\n domain=\"llm\",\n description=\"Check if LLM is configured and ready for dashboard validation and documentation\",\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"get_llm_status\")\nasync def handle_get_llm_status(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Check LLM configuration and readiness status.\"\"\"\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 lines = [\"**🤖 LLM Статус:**\"]\n\n if not providers:\n lines.append(\"❌ Провайдеры не настроены.\")\n return (\"\\n\".join(lines), None, [])\n\n lines.append(f\"📊 Всего провайдеров: {len(providers)}\")\n lines.append(f\"🟢 Активных: {len([p for p in providers if p.is_active])}\")\n lines.append(f\"⚪ Неактивных: {len([p for p in providers if not p.is_active])}\")\n\n if active_provider:\n api_key = service.get_decrypted_api_key(active_provider.id)\n from src.api.routes.llm import _is_valid_runtime_api_key\n\n key_valid = _is_valid_runtime_api_key(api_key)\n lines.append(f\"\\n**Активный провайдер:** {active_provider.name}\")\n lines.append(f\" └ Тип: {active_provider.provider_type}\")\n lines.append(f\" └ Модель: {active_provider.default_model or 'не указана'}\")\n lines.append(f\" └ API ключ: {'✅ валидный' if key_valid else '❌ невалидный/отсутствует'}\")\n lines.append(f\" └ Статус: {'✅ готов' if key_valid else '❌ настроен не полностью'}\")\n\n return (\n \"\\n\".join(lines),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Настройки LLM\",\n target=\"/settings/llm\",\n )\n ],\n )\n\n\n# #endregion handle_get_llm_status\n" + }, + { + "contract_id": "AssistantToolLlmDocumentation", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_llm_documentation.py", + "start_line": 1, + "end_line": 80, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"run_llm_documentation\" tool — generate dataset documentation via LLM.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolLlmDocumentation", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolLlmDocumentation", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolLlmDocumentation [C:3] [TYPE Module] [SEMANTICS assistant, tool, llm, documentation]\n# @BRIEF Handler for the \"run_llm_documentation\" tool — generate dataset documentation via LLM.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _resolve_env_id, _resolve_provider_id\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_run_llm_documentation [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_llm_documentation\",\n domain=\"llm\",\n description=\"Generate dataset documentation via LLM\",\n required_entities=[\"dataset_id\"],\n optional_entities=[\"environment\", \"provider\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:llm_documentation\", \"EXECUTE\")],\n)\n@belief_scope(\"run_llm_documentation\")\nasync def handle_run_llm_documentation(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Generate dataset documentation via LLM.\"\"\"\n _check_any_permission(current_user, [(\"plugin:llm_documentation\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n dataset_id = entities.get(\"dataset_id\")\n env_id = _resolve_env_id(entities.get(\"environment\"), config_manager)\n provider_id = _resolve_provider_id(\n entities.get(\"provider\"),\n db,\n config_manager=config_manager,\n task_key=\"documentation\",\n )\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(\n status_code=400, detail=\"Missing dataset_id/environment/provider\"\n )\n task = await task_manager.create_task(\n plugin_id=\"llm_documentation\",\n params={\n \"dataset_id\": str(dataset_id),\n \"environment_id\": env_id,\n \"provider_id\": provider_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"Генерация документации запущена. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_run_llm_documentation\n# #endregion AssistantToolLlmDocumentation\n" + }, + { + "contract_id": "handle_run_llm_documentation", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_llm_documentation.py", + "start_line": 24, + "end_line": 79, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_run_llm_documentation [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_llm_documentation\",\n domain=\"llm\",\n description=\"Generate dataset documentation via LLM\",\n required_entities=[\"dataset_id\"],\n optional_entities=[\"environment\", \"provider\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:llm_documentation\", \"EXECUTE\")],\n)\n@belief_scope(\"run_llm_documentation\")\nasync def handle_run_llm_documentation(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Generate dataset documentation via LLM.\"\"\"\n _check_any_permission(current_user, [(\"plugin:llm_documentation\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n dataset_id = entities.get(\"dataset_id\")\n env_id = _resolve_env_id(entities.get(\"environment\"), config_manager)\n provider_id = _resolve_provider_id(\n entities.get(\"provider\"),\n db,\n config_manager=config_manager,\n task_key=\"documentation\",\n )\n if not dataset_id or not env_id or (not provider_id):\n raise HTTPException(\n status_code=400, detail=\"Missing dataset_id/environment/provider\"\n )\n task = await task_manager.create_task(\n plugin_id=\"llm_documentation\",\n params={\n \"dataset_id\": str(dataset_id),\n \"environment_id\": env_id,\n \"provider_id\": provider_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"Генерация документации запущена. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_run_llm_documentation\n" + }, + { + "contract_id": "AssistantToolLlmValidation", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_llm_validation.py", + "start_line": 1, + "end_line": 97, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"run_llm_validation\" tool — run LLM dashboard validation.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolLlmValidation", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolLlmValidation", + "relation_type": "DEPENDS_ON", + "target_id": "LLMProviderService", + "target_ref": "[LLMProviderService]" + }, + { + "source_id": "AssistantToolLlmValidation", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolLlmValidation [C:3] [TYPE Module] [SEMANTICS assistant, tool, llm, validation]\n# @BRIEF Handler for the \"run_llm_validation\" tool — run LLM dashboard validation.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.services.llm_provider import LLMProviderService\n\nfrom ._resolvers import (\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n _resolve_provider_id,\n)\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_run_llm_validation [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_llm_validation\",\n domain=\"llm\",\n description=\"Run LLM dashboard validation by dashboard id/slug/title\",\n optional_entities=[\"dashboard_ref\", \"environment\", \"provider\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n)\n@belief_scope(\"run_llm_validation\")\nasync def handle_run_llm_validation(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run LLM dashboard validation.\"\"\"\n _check_any_permission(current_user, [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(\n None, config_manager\n )\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n provider_id = _resolve_provider_id(\n entities.get(\"provider\"),\n db,\n config_manager=config_manager,\n task_key=\"dashboard_validation\",\n )\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(\n status_code=422,\n detail=\"Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.\",\n )\n provider = LLMProviderService(db).get_provider(provider_id)\n if not provider or not bool(provider.is_multimodal):\n raise HTTPException(\n status_code=422,\n detail=\"Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).\",\n )\n task = await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": str(dashboard_id),\n \"environment_id\": env_id,\n \"provider_id\": provider_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"LLM-валидация запущена. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_run_llm_validation\n# #endregion AssistantToolLlmValidation\n" + }, + { + "contract_id": "handle_run_llm_validation", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_llm_validation.py", + "start_line": 30, + "end_line": 96, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_run_llm_validation [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"run_llm_validation\",\n domain=\"llm\",\n description=\"Run LLM dashboard validation by dashboard id/slug/title\",\n optional_entities=[\"dashboard_ref\", \"environment\", \"provider\"],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[(\"plugin:llm_dashboard_validation\", \"EXECUTE\")],\n)\n@belief_scope(\"run_llm_validation\")\nasync def handle_run_llm_validation(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run LLM dashboard validation.\"\"\"\n _check_any_permission(current_user, [(\"plugin:llm_dashboard_validation\", \"EXECUTE\")])\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n env_id = _resolve_env_id(env_token, config_manager) or _resolve_env_id(\n None, config_manager\n )\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=env_token\n )\n provider_id = _resolve_provider_id(\n entities.get(\"provider\"),\n db,\n config_manager=config_manager,\n task_key=\"dashboard_validation\",\n )\n if not dashboard_id or not env_id or (not provider_id):\n raise HTTPException(\n status_code=422,\n detail=\"Missing dashboard_id/environment/provider. Укажите ID/slug дашборда или окружение.\",\n )\n provider = LLMProviderService(db).get_provider(provider_id)\n if not provider or not bool(provider.is_multimodal):\n raise HTTPException(\n status_code=422,\n detail=\"Selected provider model is not multimodal for dashboard validation. Выберите мультимодальную модель (например, gpt-4o).\",\n )\n task = await task_manager.create_task(\n plugin_id=\"llm_dashboard_validation\",\n params={\n \"dashboard_id\": str(dashboard_id),\n \"environment_id\": env_id,\n \"provider_id\": provider_id,\n },\n user_id=current_user.id,\n )\n return (\n f\"LLM-валидация запущена. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ],\n )\n\n\n# #endregion handle_run_llm_validation\n" + }, + { + "contract_id": "AssistantToolMaintenance", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_maintenance.py", + "start_line": 1, + "end_line": 351, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handlers for maintenance operations — list events, start maintenance, end maintenance.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolMaintenance", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolMaintenance", + "relation_type": "DEPENDS_ON", + "target_id": "MaintenanceModels", + "target_ref": "[MaintenanceModels]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolMaintenance [C:3] [TYPE Module] [SEMANTICS assistant, tool, maintenance, events]\n# @BRIEF Handlers for maintenance operations — list events, start maintenance, end maintenance.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\nfrom src.models.maintenance import (\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n)\n\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_list_maintenance_events [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_maintenance_events\",\n domain=\"maintenance\",\n description=\"List active and recent maintenance events (maintenance windows) with affected tables and status\",\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"maintenance\", \"READ\")],\n)\n@belief_scope(\"list_maintenance_events\")\nasync def handle_list_maintenance_events(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"List active and recent maintenance events.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"READ\")])\n\n # Auto-expiry: end events past their end_time\n now = datetime.now(UTC)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n\n # Active events\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events (last 20)\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(20)\n .all()\n )\n\n lines = []\n\n if active_events:\n lines.append(\"**🟡 Активные события обслуживания:**\")\n for ev in active_events:\n affected = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == ev.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n status_icon = \"🟢\" if ev.status == MaintenanceEventStatus.ACTIVE else \"🟡\"\n tables = \", \".join(ev.tables[:5]) if ev.tables else \"все\"\n lines.append(\n f\"{status_icon} **{ev.id[:8]}** — {tables}\"\n f\" (дашбордов: {affected})\"\n )\n lines.append(\"\")\n\n if completed_events:\n lines.append(\"**✅ Завершённые события:**\")\n for ev in completed_events:\n icon = \"✅\" if ev.status == MaintenanceEventStatus.COMPLETED else \"❌\"\n tables = \", \".join(ev.tables[:3]) if ev.tables else \"все\"\n lines.append(f\"{icon} {ev.id[:8]} — {tables}\")\n lines.append(\"\")\n\n if not active_events and not completed_events:\n lines.append(\"Нет событий обслуживания.\")\n\n return (\n \"\\n\".join(lines).strip(),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n )\n ],\n )\n\n\n# #endregion handle_list_maintenance_events\n\n\n# #region handle_start_maintenance [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"start_maintenance\",\n domain=\"maintenance\",\n description=\"Start a maintenance window for specified tables with start/end time and optional message\",\n required_entities=[\"tables\", \"start_time\"],\n optional_entities=[\"end_time\", \"message\"],\n risk_level=\"guarded\",\n requires_confirmation=True,\n permission_checks=[(\"maintenance\", \"WRITE\")],\n)\n@belief_scope(\"start_maintenance\")\nasync def handle_start_maintenance(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Start a maintenance window.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"WRITE\")])\n entities = intent.get(\"entities\", {})\n tables = entities.get(\"tables\", [])\n start_time_str = entities.get(\"start_time\")\n end_time_str = entities.get(\"end_time\")\n message = entities.get(\"message\")\n\n if not tables or not start_time_str:\n raise HTTPException(\n status_code=422,\n detail=\"Missing tables or start_time. Укажите таблицы и время начала.\",\n )\n\n if isinstance(tables, str):\n tables = [t.strip() for t in tables.split(\",\")]\n\n # Parse times\n try:\n start_time = datetime.fromisoformat(start_time_str)\n except (ValueError, TypeError):\n raise HTTPException(\n status_code=422,\n detail=\"Invalid start_time format. Используйте ISO формат (YYYY-MM-DDTHH:MM).\",\n )\n\n end_time = None\n if end_time_str:\n try:\n end_time = datetime.fromisoformat(end_time_str)\n except (ValueError, TypeError):\n raise HTTPException(\n status_code=422,\n detail=\"Invalid end_time format. Используйте ISO формат (YYYY-MM-DDTHH:MM).\",\n )\n\n # Validate tables list\n if len(tables) > 100:\n raise HTTPException(status_code=422, detail=\"Maximum 100 tables allowed\")\n\n # Schedule maintenance via task manager\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"tables\": tables,\n \"start_time\": start_time.isoformat(),\n \"end_time\": end_time.isoformat() if end_time else None,\n \"message\": message or \"\",\n \"user_id\": current_user.id,\n },\n )\n\n table_list = \", \".join(tables[:5])\n if len(tables) > 5:\n table_list += f\" и ещё {len(tables) - 5}\"\n\n text = (\n f\"🟡 Обслуживание запущено. task_id={task.id}\\n\"\n f\"Таблицы: {table_list}\\n\"\n f\"Начало: {start_time.isoformat()}\"\n )\n if end_time:\n text += f\"\\nОкончание: {end_time.isoformat()}\"\n if message:\n text += f\"\\nСообщение: {message}\"\n\n return (\n text,\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n ),\n ],\n )\n\n\n# #endregion handle_start_maintenance\n\n\n# #region handle_end_maintenance [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"end_maintenance\",\n domain=\"maintenance\",\n description=\"End a specific maintenance event by event_id or end all active maintenance events\",\n optional_entities=[\"event_id\"],\n risk_level=\"guarded\",\n requires_confirmation=True,\n permission_checks=[(\"maintenance\", \"WRITE\")],\n)\n@belief_scope(\"end_maintenance\")\nasync def handle_end_maintenance(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"End a specific maintenance event or all active events.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"WRITE\")])\n entities = intent.get(\"entities\", {})\n event_id = entities.get(\"event_id\")\n\n if event_id:\n # End specific event\n event = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == event_id)\n .first()\n )\n if not event:\n raise HTTPException(\n status_code=404,\n detail=f\"Событие обслуживания {event_id} не найдено.\",\n )\n\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": event.environment_id,\n \"user\": current_user.id,\n },\n )\n return (\n f\"✅ Завершение обслуживания {event_id} запущено. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n ],\n )\n\n # End all active events\n active_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ])\n )\n .all()\n )\n\n if not active_events:\n return (\"Нет активных событий обслуживания.\", None, [])\n\n tasks = []\n for event in active_events:\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": event.environment_id,\n \"user\": current_user.id,\n },\n )\n tasks.append(task)\n\n return (\n f\"✅ Завершение {len(tasks)} событий обслуживания запущено.\",\n tasks[0].id if tasks else None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n ),\n ],\n )\n\n\n# #endregion handle_end_maintenance\n# #endregion AssistantToolMaintenance\n" + }, + { + "contract_id": "handle_list_maintenance_events", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_maintenance.py", + "start_line": 30, + "end_line": 150, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_list_maintenance_events [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"list_maintenance_events\",\n domain=\"maintenance\",\n description=\"List active and recent maintenance events (maintenance windows) with affected tables and status\",\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"maintenance\", \"READ\")],\n)\n@belief_scope(\"list_maintenance_events\")\nasync def handle_list_maintenance_events(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"List active and recent maintenance events.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"READ\")])\n\n # Auto-expiry: end events past their end_time\n now = datetime.now(UTC)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n\n # Active events\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events (last 20)\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(20)\n .all()\n )\n\n lines = []\n\n if active_events:\n lines.append(\"**🟡 Активные события обслуживания:**\")\n for ev in active_events:\n affected = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == ev.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n status_icon = \"🟢\" if ev.status == MaintenanceEventStatus.ACTIVE else \"🟡\"\n tables = \", \".join(ev.tables[:5]) if ev.tables else \"все\"\n lines.append(\n f\"{status_icon} **{ev.id[:8]}** — {tables}\"\n f\" (дашбордов: {affected})\"\n )\n lines.append(\"\")\n\n if completed_events:\n lines.append(\"**✅ Завершённые события:**\")\n for ev in completed_events:\n icon = \"✅\" if ev.status == MaintenanceEventStatus.COMPLETED else \"❌\"\n tables = \", \".join(ev.tables[:3]) if ev.tables else \"все\"\n lines.append(f\"{icon} {ev.id[:8]} — {tables}\")\n lines.append(\"\")\n\n if not active_events and not completed_events:\n lines.append(\"Нет событий обслуживания.\")\n\n return (\n \"\\n\".join(lines).strip(),\n None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n )\n ],\n )\n\n\n# #endregion handle_list_maintenance_events\n" + }, + { + "contract_id": "handle_start_maintenance", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_maintenance.py", + "start_line": 153, + "end_line": 253, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_start_maintenance [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"start_maintenance\",\n domain=\"maintenance\",\n description=\"Start a maintenance window for specified tables with start/end time and optional message\",\n required_entities=[\"tables\", \"start_time\"],\n optional_entities=[\"end_time\", \"message\"],\n risk_level=\"guarded\",\n requires_confirmation=True,\n permission_checks=[(\"maintenance\", \"WRITE\")],\n)\n@belief_scope(\"start_maintenance\")\nasync def handle_start_maintenance(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Start a maintenance window.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"WRITE\")])\n entities = intent.get(\"entities\", {})\n tables = entities.get(\"tables\", [])\n start_time_str = entities.get(\"start_time\")\n end_time_str = entities.get(\"end_time\")\n message = entities.get(\"message\")\n\n if not tables or not start_time_str:\n raise HTTPException(\n status_code=422,\n detail=\"Missing tables or start_time. Укажите таблицы и время начала.\",\n )\n\n if isinstance(tables, str):\n tables = [t.strip() for t in tables.split(\",\")]\n\n # Parse times\n try:\n start_time = datetime.fromisoformat(start_time_str)\n except (ValueError, TypeError):\n raise HTTPException(\n status_code=422,\n detail=\"Invalid start_time format. Используйте ISO формат (YYYY-MM-DDTHH:MM).\",\n )\n\n end_time = None\n if end_time_str:\n try:\n end_time = datetime.fromisoformat(end_time_str)\n except (ValueError, TypeError):\n raise HTTPException(\n status_code=422,\n detail=\"Invalid end_time format. Используйте ISO формат (YYYY-MM-DDTHH:MM).\",\n )\n\n # Validate tables list\n if len(tables) > 100:\n raise HTTPException(status_code=422, detail=\"Maximum 100 tables allowed\")\n\n # Schedule maintenance via task manager\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"tables\": tables,\n \"start_time\": start_time.isoformat(),\n \"end_time\": end_time.isoformat() if end_time else None,\n \"message\": message or \"\",\n \"user_id\": current_user.id,\n },\n )\n\n table_list = \", \".join(tables[:5])\n if len(tables) > 5:\n table_list += f\" и ещё {len(tables) - 5}\"\n\n text = (\n f\"🟡 Обслуживание запущено. task_id={task.id}\\n\"\n f\"Таблицы: {table_list}\\n\"\n f\"Начало: {start_time.isoformat()}\"\n )\n if end_time:\n text += f\"\\nОкончание: {end_time.isoformat()}\"\n if message:\n text += f\"\\nСообщение: {message}\"\n\n return (\n text,\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n ),\n ],\n )\n\n\n# #endregion handle_start_maintenance\n" + }, + { + "contract_id": "handle_end_maintenance", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_maintenance.py", + "start_line": 256, + "end_line": 350, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_end_maintenance [C:2] [TYPE Function]\n@assistant_tool(\n operation=\"end_maintenance\",\n domain=\"maintenance\",\n description=\"End a specific maintenance event by event_id or end all active maintenance events\",\n optional_entities=[\"event_id\"],\n risk_level=\"guarded\",\n requires_confirmation=True,\n permission_checks=[(\"maintenance\", \"WRITE\")],\n)\n@belief_scope(\"end_maintenance\")\nasync def handle_end_maintenance(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"End a specific maintenance event or all active events.\"\"\"\n _check_any_permission(current_user, [(\"maintenance\", \"WRITE\")])\n entities = intent.get(\"entities\", {})\n event_id = entities.get(\"event_id\")\n\n if event_id:\n # End specific event\n event = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == event_id)\n .first()\n )\n if not event:\n raise HTTPException(\n status_code=404,\n detail=f\"Событие обслуживания {event_id} не найдено.\",\n )\n\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": event.environment_id,\n \"user\": current_user.id,\n },\n )\n return (\n f\"✅ Завершение обслуживания {event_id} запущено. task_id={task.id}\",\n task.id,\n [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n ],\n )\n\n # End all active events\n active_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ])\n )\n .all()\n )\n\n if not active_events:\n return (\"Нет активных событий обслуживания.\", None, [])\n\n tasks = []\n for event in active_events:\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": event.environment_id,\n \"user\": current_user.id,\n },\n )\n tasks.append(task)\n\n return (\n f\"✅ Завершение {len(tasks)} событий обслуживания запущено.\",\n tasks[0].id if tasks else None,\n [\n AssistantAction(\n type=\"open_route\",\n label=\"Открыть Maintenance\",\n target=\"/maintenance\",\n ),\n ],\n )\n\n\n# #endregion handle_end_maintenance\n" + }, + { + "contract_id": "AssistantToolMigration", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_migration.py", + "start_line": 1, + "end_line": 118, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "BRIEF": "Handler for the \"execute_migration\" tool — run dashboard migration between environments.", + "COMPLEXITY": 4, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolMigration", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolMigration", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolMigration [C:4] [TYPE Module] [SEMANTICS assistant, tool, migration, execute]\n# @BRIEF Handler for the \"execute_migration\" tool — run dashboard migration between environments.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._history import _coerce_query_bool\nfrom ._resolvers import (\n _get_environment_name_by_id,\n _resolve_dashboard_id_entity,\n _resolve_env_id,\n)\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_execute_migration [C:4] [TYPE Function]\n@assistant_tool(\n operation=\"execute_migration\",\n domain=\"migration\",\n description=(\n \"Run dashboard migration (id/slug/title) between environments. \"\n \"Optional boolean flags: replace_db_config, fix_cross_filters\"\n ),\n required_entities=[\"source_env\", \"target_env\"],\n optional_entities=[\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n)\n@belief_scope(\"execute_migration\")\nasync def handle_execute_migration(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run dashboard migration between environments.\"\"\"\n _check_any_permission(\n current_user,\n [(\"plugin:migration\", \"EXECUTE\"), (\"plugin:superset-migration\", \"EXECUTE\")],\n )\n entities = intent.get(\"entities\", {})\n src_token = entities.get(\"source_env\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=src_token\n )\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] = {\n \"source_env_id\": src,\n \"target_env_id\": tgt,\n \"replace_db_config\": _coerce_query_bool(\n entities.get(\"replace_db_config\", False)\n ),\n \"fix_cross_filters\": _coerce_query_bool(\n entities.get(\"fix_cross_filters\", True)\n ),\n }\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(\n plugin_id=\"superset-migration\",\n params=migration_params,\n user_id=current_user.id,\n )\n actions: list[AssistantAction] = [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ]\n if dashboard_id:\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}\",\n target=f\"/dashboards/{dashboard_id}?env_id={tgt}\",\n )\n )\n actions.append(\n AssistantAction(\n type=\"open_diff\", label=\"Показать Diff\", target=str(dashboard_id)\n )\n )\n return (f\"Миграция запущена. task_id={task.id}\", task.id, actions)\n\n\n# #endregion handle_execute_migration\n# #endregion AssistantToolMigration\n" + }, + { + "contract_id": "handle_execute_migration", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_migration.py", + "start_line": 29, + "end_line": 117, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "COMPLEXITY": 4 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_execute_migration [C:4] [TYPE Function]\n@assistant_tool(\n operation=\"execute_migration\",\n domain=\"migration\",\n description=(\n \"Run dashboard migration (id/slug/title) between environments. \"\n \"Optional boolean flags: replace_db_config, fix_cross_filters\"\n ),\n required_entities=[\"source_env\", \"target_env\"],\n optional_entities=[\n \"dashboard_id\",\n \"dashboard_ref\",\n \"replace_db_config\",\n \"fix_cross_filters\",\n ],\n risk_level=\"guarded\",\n requires_confirmation=False,\n permission_checks=[\n (\"plugin:migration\", \"EXECUTE\"),\n (\"plugin:superset-migration\", \"EXECUTE\"),\n ],\n)\n@belief_scope(\"execute_migration\")\nasync def handle_execute_migration(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Run dashboard migration between environments.\"\"\"\n _check_any_permission(\n current_user,\n [(\"plugin:migration\", \"EXECUTE\"), (\"plugin:superset-migration\", \"EXECUTE\")],\n )\n entities = intent.get(\"entities\", {})\n src_token = entities.get(\"source_env\")\n dashboard_ref = entities.get(\"dashboard_ref\")\n dashboard_id = _resolve_dashboard_id_entity(\n entities, config_manager, env_hint=src_token\n )\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] = {\n \"source_env_id\": src,\n \"target_env_id\": tgt,\n \"replace_db_config\": _coerce_query_bool(\n entities.get(\"replace_db_config\", False)\n ),\n \"fix_cross_filters\": _coerce_query_bool(\n entities.get(\"fix_cross_filters\", True)\n ),\n }\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(\n plugin_id=\"superset-migration\",\n params=migration_params,\n user_id=current_user.id,\n )\n actions: list[AssistantAction] = [\n AssistantAction(type=\"open_task\", label=\"Open Task\", target=task.id),\n AssistantAction(\n type=\"open_reports\", label=\"Open Reports\", target=\"/reports\"\n ),\n ]\n if dashboard_id:\n actions.append(\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборд в {_get_environment_name_by_id(tgt, config_manager)}\",\n target=f\"/dashboards/{dashboard_id}?env_id={tgt}\",\n )\n )\n actions.append(\n AssistantAction(\n type=\"open_diff\", label=\"Показать Diff\", target=str(dashboard_id)\n )\n )\n return (f\"Миграция запущена. task_id={task.id}\", task.id, actions)\n\n\n# #endregion handle_execute_migration\n" + }, + { + "contract_id": "AssistantToolRegistry", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 1, + "end_line": 268, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "BRIEF": "Central `@assistant_tool` decorator-based registry: auto-generates LLM catalog, dispatches operations,", + "COMPLEXITY": 4 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolRegistry [C:4] [TYPE Module] [SEMANTICS assistant, registry, tool, dispatch, catalog]\n# @BRIEF Central `@assistant_tool` decorator-based registry: auto-generates LLM catalog, dispatches operations,\n# and enforces permissions. Replaces hand-maintained `_build_tool_catalog()`, `_dispatch_intent()`,\n# `INTENT_PERMISSION_CHECKS`, and `_SAFE_OPS` to eliminate cross-file desynchronisation.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantSchemas]\n# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]\n# @RELATION DISPATCHES -> [AssistantToolCapabilities]\n# @RELATION DISPATCHES -> [AssistantToolTaskStatus]\n# @RELATION DISPATCHES -> [AssistantToolCreateBranch]\n# @RELATION DISPATCHES -> [AssistantToolCommit]\n# @RELATION DISPATCHES -> [AssistantToolDeploy]\n# @RELATION DISPATCHES -> [AssistantToolMigration]\n# @RELATION DISPATCHES -> [AssistantToolBackup]\n# @RELATION DISPATCHES -> [AssistantToolLlmValidation]\n# @RELATION DISPATCHES -> [AssistantToolLlmDocumentation]\n# @RELATION DISPATCHES -> [AssistantToolHealthSummary]\n# @DATA_CONTRACT Handler[intent, current_user, task_manager, config_manager, db] -> (text: str, task_id: str | None, actions: list[AssistantAction])\n# @INVARIANT Every registered tool has exactly one handler. Unsupported operations raise HTTPException(400).\n# @INVARIANT Permission checks are mandatory metadata — not an afterthought.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import Any, Callable, Coroutine\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope\nfrom src.core.task_manager import TaskManager\nfrom src.dependencies import has_permission\nfrom src.schemas.auth import User\n\nfrom ._schemas import AssistantAction\n\n# Type alias for the handler signature\nToolHandler = Callable[\n ...,\n Coroutine[Any, Any, tuple[str, str | None, list[AssistantAction]]],\n]\n\n\n# #region AssistantTool [C:1] [TYPE Class]\n@dataclass\nclass AssistantTool:\n \"\"\"Registered assistant tool metadata + handler.\"\"\"\n\n operation: str\n domain: str\n description: str\n handler: ToolHandler\n required_entities: list[str] = field(default_factory=list)\n optional_entities: list[str] = field(default_factory=list)\n risk_level: str = \"safe\"\n requires_confirmation: bool = False\n permission_checks: list[tuple[str, str]] = field(default_factory=list)\n defaults: dict[str, Any] | None = None\n\n\n# #endregion AssistantTool\n\n\n# #region _tools [C:1] [TYPE Constant]\n# @BRIEF Central tool registry: operation_name -> AssistantTool.\n_tools: dict[str, AssistantTool] = {}\n# #endregion _tools\n\n\n# #region assistant_tool [C:2] [TYPE Function]\n# @BRIEF Decorator that registers a function as an assistant tool handler.\n# @POST Tool is registered in `_tools` by operation name. Handler gets `__assistant_tool__` attr.\n# @SIDE_EFFECT Mutates the module-level `_tools` dict.\ndef assistant_tool(\n *,\n operation: str,\n domain: str,\n description: str,\n required_entities: list[str] | None = None,\n optional_entities: list[str] | None = None,\n risk_level: str = \"safe\",\n requires_confirmation: bool = False,\n permission_checks: list[tuple[str, str]] | None = None,\n defaults: dict[str, Any] | None = None,\n):\n def decorator(func):\n tool = AssistantTool(\n operation=operation,\n domain=domain,\n description=description,\n handler=func,\n required_entities=required_entities or [],\n optional_entities=optional_entities or [],\n risk_level=risk_level,\n requires_confirmation=requires_confirmation,\n permission_checks=permission_checks or [],\n defaults=defaults,\n )\n _tools[operation] = tool\n func.__assistant_tool__ = tool # type: ignore[attr-defined]\n return func\n\n return decorator\n\n\n# #endregion assistant_tool\n\n\n# ── Permission helpers ─────────────────────────────────────────────────────\n\n\n# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE checks list contains resource-action tuples.\n# @POST Returns on first successful permission; raises 403-like HTTPException.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n\n\n# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Boolean variant of _check_any_permission.\n# @POST Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n\n\n# ── Derived lists (replaces _SAFE_OPS, INTENT_PERMISSION_CHECKS) ────────────\n\n\n# #region get_safe_ops [C:1] [TYPE Function] [SEMANTICS safelist,operations]\n# @BRIEF Return set of operation names with risk_level == 'safe'.\n# @POST Includes registered tool safe-ops plus known non-registry safe operations\n# (e.g. dataset_review_answer_context) that bypass the confirmation gate.\ndef get_safe_ops() -> set[str]:\n \"\"\"Return set of operation names with risk_level == 'safe'.\n\n Includes registered tool safe-ops plus known non-registry safe operations\n that bypass the confirmation gate.\n \"\"\"\n ops = {op for op, t in _tools.items() if t.risk_level == \"safe\"}\n # dataset_review_answer_context is not registered via @assistant_tool\n # (it's dispatched via special-case path) but is read-only / safe.\n ops.add(\"dataset_review_answer_context\")\n return ops\n\n\n# #endregion get_safe_ops\n\n\n# #region get_permission_checks [C:1] [TYPE Function] [SEMANTICS permissions,authorization]\n# @BRIEF Return dict of operation_name -> permission_checks list for all registered tools.\n# @POST Returns mapping used by _authorize_intent and catalog filtering.\ndef get_permission_checks() -> dict[str, list[tuple[str, str]]]:\n \"\"\"Return dict of operation_name -> permission_checks list.\"\"\"\n result: dict[str, list[tuple[str, str]]] = {}\n for op, t in _tools.items():\n if t.permission_checks:\n result[op] = t.permission_checks\n # Also include dataset_review operations that route via special-case dispatch\n from ._schemas import _DATASET_REVIEW_OPS, INTENT_PERMISSION_CHECKS\n for op in _DATASET_REVIEW_OPS:\n if op in INTENT_PERMISSION_CHECKS:\n result[op] = INTENT_PERMISSION_CHECKS[op]\n if \"dataset_review_answer_context\" in INTENT_PERMISSION_CHECKS:\n result[\"dataset_review_answer_context\"] = INTENT_PERMISSION_CHECKS[\"dataset_review_answer_context\"]\n return result\n\n\n# #endregion get_permission_checks\n\n\n# ── Catalog generation ─────────────────────────────────────────────────────\n\n\n# #region get_catalog [C:2] [TYPE Function]\n# @BRIEF Build LLM tool catalog from registry, filtered by user permissions.\n# @POST Returns list of dicts suitable for LLM planner prompt.\ndef get_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> list[dict[str, Any]]:\n \"\"\"Build LLM tool catalog from registry, filtered by user permissions.\"\"\"\n result: list[dict[str, Any]] = []\n for op, tool in sorted(_tools.items(), key=lambda x: x[0]):\n if tool.permission_checks and not _has_any_permission(\n current_user, tool.permission_checks\n ):\n continue\n entry: dict[str, Any] = {\n \"operation\": op,\n \"domain\": tool.domain,\n \"description\": tool.description,\n \"required_entities\": tool.required_entities,\n \"optional_entities\": tool.optional_entities,\n \"risk_level\": tool.risk_level,\n \"requires_confirmation\": tool.requires_confirmation,\n }\n if tool.defaults:\n entry[\"defaults\"] = tool.defaults\n result.append(entry)\n return result\n\n\n# #endregion get_catalog\n\n\n# ── Dispatch ────────────────────────────────────────────────────────────────\n\n\n# #region dispatch [C:2] [TYPE Function]\n# @BRIEF Look up operation in registry and call its handler.\n# @PRE operation is a known registered tool.\n# @POST Returns (text, task_id, actions) from the handler.\nasync def dispatch(\n operation: str,\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Look up operation in registry and call its handler.\"\"\"\n with belief_scope(\"tool_registry.dispatch\"):\n from ._dataset_review_dispatch import _dispatch_dataset_review_intent\n from ._schemas import _DATASET_REVIEW_OPS\n\n # Special case: dataset review operations are dispatched through\n # a dedicated sub-dispatcher to isolate session-scoped logic.\n if operation in _DATASET_REVIEW_OPS or operation == \"dataset_review_answer_context\":\n return await _dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n )\n\n tool = _tools.get(operation)\n if not tool:\n raise HTTPException(status_code=400, detail=f\"Unsupported operation: {operation}\")\n\n return await tool.handler(intent, current_user, task_manager, config_manager, db)\n\n\n# #endregion dispatch\n\n\n# #endregion AssistantToolRegistry\n" + }, + { + "contract_id": "AssistantTool", + "contract_type": "Class", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 45, + "end_line": 62, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantTool [C:1] [TYPE Class]\n@dataclass\nclass AssistantTool:\n \"\"\"Registered assistant tool metadata + handler.\"\"\"\n\n operation: str\n domain: str\n description: str\n handler: ToolHandler\n required_entities: list[str] = field(default_factory=list)\n optional_entities: list[str] = field(default_factory=list)\n risk_level: str = \"safe\"\n requires_confirmation: bool = False\n permission_checks: list[tuple[str, str]] = field(default_factory=list)\n defaults: dict[str, Any] | None = None\n\n\n# #endregion AssistantTool\n" + }, + { + "contract_id": "_tools", + "contract_type": "Constant", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 65, + "end_line": 68, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "Central tool registry: operation_name -> AssistantTool.", + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _tools [C:1] [TYPE Constant]\n# @BRIEF Central tool registry: operation_name -> AssistantTool.\n_tools: dict[str, AssistantTool] = {}\n# #endregion _tools\n" + }, + { + "contract_id": "assistant_tool", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 71, + "end_line": 107, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Decorator that registers a function as an assistant tool handler.", + "COMPLEXITY": 2, + "POST": "Tool is registered in `_tools` by operation name. Handler gets `__assistant_tool__` attr.", + "SIDE_EFFECT": "Mutates the module-level `_tools` dict." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region assistant_tool [C:2] [TYPE Function]\n# @BRIEF Decorator that registers a function as an assistant tool handler.\n# @POST Tool is registered in `_tools` by operation name. Handler gets `__assistant_tool__` attr.\n# @SIDE_EFFECT Mutates the module-level `_tools` dict.\ndef assistant_tool(\n *,\n operation: str,\n domain: str,\n description: str,\n required_entities: list[str] | None = None,\n optional_entities: list[str] | None = None,\n risk_level: str = \"safe\",\n requires_confirmation: bool = False,\n permission_checks: list[tuple[str, str]] | None = None,\n defaults: dict[str, Any] | None = None,\n):\n def decorator(func):\n tool = AssistantTool(\n operation=operation,\n domain=domain,\n description=description,\n handler=func,\n required_entities=required_entities or [],\n optional_entities=optional_entities or [],\n risk_level=risk_level,\n requires_confirmation=requires_confirmation,\n permission_checks=permission_checks or [],\n defaults=defaults,\n )\n _tools[operation] = tool\n func.__assistant_tool__ = tool # type: ignore[attr-defined]\n return func\n\n return decorator\n\n\n# #endregion assistant_tool\n" + }, + { + "contract_id": "_check_any_permission", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 113, + "end_line": 133, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Validate user against alternative permission checks (logical OR).", + "COMPLEXITY": 2, + "POST": "Returns on first successful permission; raises 403-like HTTPException.", + "PRE": "checks list contains resource-action tuples." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _check_any_permission [C:2] [TYPE Function]\n# @BRIEF Validate user against alternative permission checks (logical OR).\n# @PRE checks list contains resource-action tuples.\n# @POST Returns on first successful permission; raises 403-like HTTPException.\ndef _check_any_permission(current_user: User, checks: list[tuple[str, str]]):\n errors: list[HTTPException] = []\n for resource, action in checks:\n try:\n has_permission(resource, action)(current_user)\n return\n except HTTPException as exc:\n errors.append(exc)\n\n raise (\n errors[-1]\n if errors\n else HTTPException(status_code=403, detail=\"Permission denied\")\n )\n\n\n# #endregion _check_any_permission\n" + }, + { + "contract_id": "_has_any_permission", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 136, + "end_line": 147, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Boolean variant of _check_any_permission.", + "COMPLEXITY": 2, + "POST": "Returns True when at least one permission check passes." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _has_any_permission [C:2] [TYPE Function]\n# @BRIEF Boolean variant of _check_any_permission.\n# @POST Returns True when at least one permission check passes.\ndef _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:\n try:\n _check_any_permission(current_user, checks)\n return True\n except HTTPException:\n return False\n\n\n# #endregion _has_any_permission\n" + }, + { + "contract_id": "get_safe_ops", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 153, + "end_line": 170, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "Return set of operation names with risk_level == 'safe'.", + "COMPLEXITY": 1, + "POST": "Includes registered tool safe-ops plus known non-registry safe operations" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region get_safe_ops [C:1] [TYPE Function] [SEMANTICS safelist,operations]\n# @BRIEF Return set of operation names with risk_level == 'safe'.\n# @POST Includes registered tool safe-ops plus known non-registry safe operations\n# (e.g. dataset_review_answer_context) that bypass the confirmation gate.\ndef get_safe_ops() -> set[str]:\n \"\"\"Return set of operation names with risk_level == 'safe'.\n\n Includes registered tool safe-ops plus known non-registry safe operations\n that bypass the confirmation gate.\n \"\"\"\n ops = {op for op, t in _tools.items() if t.risk_level == \"safe\"}\n # dataset_review_answer_context is not registered via @assistant_tool\n # (it's dispatched via special-case path) but is read-only / safe.\n ops.add(\"dataset_review_answer_context\")\n return ops\n\n\n# #endregion get_safe_ops\n" + }, + { + "contract_id": "get_permission_checks", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 173, + "end_line": 192, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "Return dict of operation_name -> permission_checks list for all registered tools.", + "COMPLEXITY": 1, + "POST": "Returns mapping used by _authorize_intent and catalog filtering." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region get_permission_checks [C:1] [TYPE Function] [SEMANTICS permissions,authorization]\n# @BRIEF Return dict of operation_name -> permission_checks list for all registered tools.\n# @POST Returns mapping used by _authorize_intent and catalog filtering.\ndef get_permission_checks() -> dict[str, list[tuple[str, str]]]:\n \"\"\"Return dict of operation_name -> permission_checks list.\"\"\"\n result: dict[str, list[tuple[str, str]]] = {}\n for op, t in _tools.items():\n if t.permission_checks:\n result[op] = t.permission_checks\n # Also include dataset_review operations that route via special-case dispatch\n from ._schemas import _DATASET_REVIEW_OPS, INTENT_PERMISSION_CHECKS\n for op in _DATASET_REVIEW_OPS:\n if op in INTENT_PERMISSION_CHECKS:\n result[op] = INTENT_PERMISSION_CHECKS[op]\n if \"dataset_review_answer_context\" in INTENT_PERMISSION_CHECKS:\n result[\"dataset_review_answer_context\"] = INTENT_PERMISSION_CHECKS[\"dataset_review_answer_context\"]\n return result\n\n\n# #endregion get_permission_checks\n" + }, + { + "contract_id": "get_catalog", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 198, + "end_line": 228, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Build LLM tool catalog from registry, filtered by user permissions.", + "COMPLEXITY": 2, + "POST": "Returns list of dicts suitable for LLM planner prompt." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region get_catalog [C:2] [TYPE Function]\n# @BRIEF Build LLM tool catalog from registry, filtered by user permissions.\n# @POST Returns list of dicts suitable for LLM planner prompt.\ndef get_catalog(\n current_user: User,\n config_manager: ConfigManager,\n db: Session,\n) -> list[dict[str, Any]]:\n \"\"\"Build LLM tool catalog from registry, filtered by user permissions.\"\"\"\n result: list[dict[str, Any]] = []\n for op, tool in sorted(_tools.items(), key=lambda x: x[0]):\n if tool.permission_checks and not _has_any_permission(\n current_user, tool.permission_checks\n ):\n continue\n entry: dict[str, Any] = {\n \"operation\": op,\n \"domain\": tool.domain,\n \"description\": tool.description,\n \"required_entities\": tool.required_entities,\n \"optional_entities\": tool.optional_entities,\n \"risk_level\": tool.risk_level,\n \"requires_confirmation\": tool.requires_confirmation,\n }\n if tool.defaults:\n entry[\"defaults\"] = tool.defaults\n result.append(entry)\n return result\n\n\n# #endregion get_catalog\n" + }, + { + "contract_id": "dispatch", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_registry.py", + "start_line": 234, + "end_line": 265, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Look up operation in registry and call its handler.", + "COMPLEXITY": 2, + "POST": "Returns (text, task_id, actions) from the handler.", + "PRE": "operation is a known registered tool." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region dispatch [C:2] [TYPE Function]\n# @BRIEF Look up operation in registry and call its handler.\n# @PRE operation is a known registered tool.\n# @POST Returns (text, task_id, actions) from the handler.\nasync def dispatch(\n operation: str,\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Look up operation in registry and call its handler.\"\"\"\n with belief_scope(\"tool_registry.dispatch\"):\n from ._dataset_review_dispatch import _dispatch_dataset_review_intent\n from ._schemas import _DATASET_REVIEW_OPS\n\n # Special case: dataset review operations are dispatched through\n # a dedicated sub-dispatcher to isolate session-scoped logic.\n if operation in _DATASET_REVIEW_OPS or operation == \"dataset_review_answer_context\":\n return await _dispatch_dataset_review_intent(\n intent, current_user, config_manager, db\n )\n\n tool = _tools.get(operation)\n if not tool:\n raise HTTPException(status_code=400, detail=f\"Unsupported operation: {operation}\")\n\n return await tool.handler(intent, current_user, task_manager, config_manager, db)\n\n\n# #endregion dispatch\n" + }, + { + "contract_id": "AssistantToolSearchDashboards", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_search_dashboards.py", + "start_line": 1, + "end_line": 120, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"search_dashboards\" tool — search and list dashboards from an environment.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolSearchDashboards", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolSearchDashboards", + "relation_type": "DEPENDS_ON", + "target_id": "SupersetClient", + "target_ref": "[SupersetClient]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolSearchDashboards [C:3] [TYPE Module] [SEMANTICS assistant, tool, dashboards, search]\n# @BRIEF Handler for the \"search_dashboards\" tool — search and list dashboards from an environment.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _resolve_env_id\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import assistant_tool\n\n\n# #region handle_search_dashboards [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"search_dashboards\",\n domain=\"dashboards\",\n description=\"Search and list dashboards by name/id from an environment. Shows id, title, and URL for each dashboard.\",\n optional_entities=[\"environment\", \"search\", \"page\", \"page_size\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"search_dashboards\")\nasync def handle_search_dashboards(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Search and list dashboards from an environment.\"\"\"\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n search = entities.get(\"search\")\n page = int(entities.get(\"page\", 1))\n page_size = min(int(entities.get(\"page_size\", 20)), 100)\n\n env_id = _resolve_env_id(env_token, config_manager)\n\n if not env_id:\n # Show available environments if none specified\n envs = config_manager.get_environments()\n if not envs:\n return (\"Нет доступных окружений.\", None, [])\n env_list = \"\\n\".join(f\"- {e.id} ({e.name})\" for e in envs)\n return (\n f\"Укажите окружение. Доступные:\\n{env_list}\\n\\n\"\n f\"Пример: `покажи дашборды из окружения dev`\",\n None,\n [],\n )\n\n env_map = {e.id: e for e in config_manager.get_environments()}\n env = env_map.get(env_id)\n if not env:\n return (f\"Окружение {env_id} не найдено.\", None, [])\n\n client = SupersetClient(env)\n all_dashboards = client.get_dashboards_summary(require_slug=True)\n\n # Filter by search term\n if search:\n search_lower = search.lower()\n filtered = [\n d\n for d in all_dashboards\n if search_lower in str(d.get(\"id\", \"\"))\n or search_lower in str(d.get(\"title\", \"\")).lower()\n or search_lower in str(d.get(\"slug\", \"\")).lower()\n ]\n else:\n filtered = list(all_dashboards)\n\n total = len(filtered)\n start = (page - 1) * page_size\n page_items = filtered[start : start + page_size]\n\n if not page_items:\n if search:\n return (\n f\"Дашборды по запросу «{search}» в окружении {env.name} не найдены.\",\n None,\n [],\n )\n return (f\"В окружении {env.name} нет дашбордов.\", None, [])\n\n lines = [f\"📊 Дашборды в окружении **{env.name}** ({total} всего):\"]\n for d in page_items:\n title = d.get(\"title\", \"Без названия\")\n did = d.get(\"id\", \"?\")\n slug = d.get(\"slug\", \"\")\n slug_str = f\" ({slug})\" if slug else \"\"\n lines.append(f\"- #{did} {title}{slug_str}\")\n\n if start + page_size < total:\n lines.append(f\"\\n... и ещё {total - start - page_size}\")\n\n actions = [\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборды в {env.name}\",\n target=f\"/dashboards?env_id={env_id}\",\n )\n ]\n\n return (\"\\n\".join(lines), None, actions)\n\n\n# #endregion handle_search_dashboards\n# #endregion AssistantToolSearchDashboards\n" + }, + { + "contract_id": "handle_search_dashboards", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_search_dashboards.py", + "start_line": 24, + "end_line": 119, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_search_dashboards [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"search_dashboards\",\n domain=\"dashboards\",\n description=\"Search and list dashboards by name/id from an environment. Shows id, title, and URL for each dashboard.\",\n optional_entities=[\"environment\", \"search\", \"page\", \"page_size\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n)\n@belief_scope(\"search_dashboards\")\nasync def handle_search_dashboards(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Search and list dashboards from an environment.\"\"\"\n entities = intent.get(\"entities\", {})\n env_token = entities.get(\"environment\")\n search = entities.get(\"search\")\n page = int(entities.get(\"page\", 1))\n page_size = min(int(entities.get(\"page_size\", 20)), 100)\n\n env_id = _resolve_env_id(env_token, config_manager)\n\n if not env_id:\n # Show available environments if none specified\n envs = config_manager.get_environments()\n if not envs:\n return (\"Нет доступных окружений.\", None, [])\n env_list = \"\\n\".join(f\"- {e.id} ({e.name})\" for e in envs)\n return (\n f\"Укажите окружение. Доступные:\\n{env_list}\\n\\n\"\n f\"Пример: `покажи дашборды из окружения dev`\",\n None,\n [],\n )\n\n env_map = {e.id: e for e in config_manager.get_environments()}\n env = env_map.get(env_id)\n if not env:\n return (f\"Окружение {env_id} не найдено.\", None, [])\n\n client = SupersetClient(env)\n all_dashboards = client.get_dashboards_summary(require_slug=True)\n\n # Filter by search term\n if search:\n search_lower = search.lower()\n filtered = [\n d\n for d in all_dashboards\n if search_lower in str(d.get(\"id\", \"\"))\n or search_lower in str(d.get(\"title\", \"\")).lower()\n or search_lower in str(d.get(\"slug\", \"\")).lower()\n ]\n else:\n filtered = list(all_dashboards)\n\n total = len(filtered)\n start = (page - 1) * page_size\n page_items = filtered[start : start + page_size]\n\n if not page_items:\n if search:\n return (\n f\"Дашборды по запросу «{search}» в окружении {env.name} не найдены.\",\n None,\n [],\n )\n return (f\"В окружении {env.name} нет дашбордов.\", None, [])\n\n lines = [f\"📊 Дашборды в окружении **{env.name}** ({total} всего):\"]\n for d in page_items:\n title = d.get(\"title\", \"Без названия\")\n did = d.get(\"id\", \"?\")\n slug = d.get(\"slug\", \"\")\n slug_str = f\" ({slug})\" if slug else \"\"\n lines.append(f\"- #{did} {title}{slug_str}\")\n\n if start + page_size < total:\n lines.append(f\"\\n... и ещё {total - start - page_size}\")\n\n actions = [\n AssistantAction(\n type=\"open_route\",\n label=f\"Открыть дашборды в {env.name}\",\n target=f\"/dashboards?env_id={env_id}\",\n )\n ]\n\n return (\"\\n\".join(lines), None, actions)\n\n\n# #endregion handle_search_dashboards\n" + }, + { + "contract_id": "AssistantToolTaskStatus", + "contract_type": "Module", + "file_path": "backend/src/api/routes/assistant/_tool_task_status.py", + "start_line": 1, + "end_line": 79, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Handler for the \"get_task_status\" tool — retrieve task status by id or latest user task.", + "COMPLEXITY": 3, + "LAYER": "API" + }, + "relations": [ + { + "source_id": "AssistantToolTaskStatus", + "relation_type": "DEPENDS_ON", + "target_id": "AssistantToolRegistry", + "target_ref": "[AssistantToolRegistry]" + }, + { + "source_id": "AssistantToolTaskStatus", + "relation_type": "DEPENDS_ON", + "target_id": "TaskManager", + "target_ref": "[TaskManager]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region AssistantToolTaskStatus [C:3] [TYPE Module] [SEMANTICS assistant, tool, status, task]\n# @BRIEF Handler for the \"get_task_status\" tool — retrieve task status by id or latest user task.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AssistantToolRegistry]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.schemas.auth import User\n\nfrom ._resolvers import _build_task_observability_summary, _extract_result_deep_links\nfrom ._schemas import AssistantAction\nfrom ._tool_registry import _check_any_permission, assistant_tool\n\n\n# #region handle_get_task_status [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"get_task_status\",\n domain=\"status\",\n description=\"Get task status by task_id or latest user task\",\n optional_entities=[\"task_id\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"tasks\", \"READ\")],\n)\n@belief_scope(\"get_task_status\")\nasync def handle_get_task_status(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Get task status by task_id or latest user task.\"\"\"\n _check_any_permission(current_user, [(\"tasks\", \"READ\")])\n entities = intent.get(\"entities\", {})\n task_id = entities.get(\"task_id\")\n if not task_id:\n recent = [\n t\n for t in task_manager.get_tasks(limit=20, offset=0)\n if t.user_id == current_user.id\n ]\n if not recent:\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 text = (\n f\"Последняя задача: {task.id}, статус: {task.status}.\"\n + (f\"\\n{summary_line}\" if summary_line else \"\")\n )\n return (text, 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 text = (\n f\"Статус задачи {task.id}: {task.status}.\"\n + (f\"\\n{summary_line}\" if summary_line else \"\")\n )\n return (text, task.id, actions)\n\n\n# #endregion handle_get_task_status\n# #endregion AssistantToolTaskStatus\n" + }, + { + "contract_id": "handle_get_task_status", + "contract_type": "Function", + "file_path": "backend/src/api/routes/assistant/_tool_task_status.py", + "start_line": 24, + "end_line": 78, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region handle_get_task_status [C:3] [TYPE Function]\n@assistant_tool(\n operation=\"get_task_status\",\n domain=\"status\",\n description=\"Get task status by task_id or latest user task\",\n optional_entities=[\"task_id\"],\n risk_level=\"safe\",\n requires_confirmation=False,\n permission_checks=[(\"tasks\", \"READ\")],\n)\n@belief_scope(\"get_task_status\")\nasync def handle_get_task_status(\n intent: dict[str, Any],\n current_user: User,\n task_manager: TaskManager,\n config_manager: ConfigManager,\n db: Session,\n) -> tuple[str, str | None, list[AssistantAction]]:\n \"\"\"Get task status by task_id or latest user task.\"\"\"\n _check_any_permission(current_user, [(\"tasks\", \"READ\")])\n entities = intent.get(\"entities\", {})\n task_id = entities.get(\"task_id\")\n if not task_id:\n recent = [\n t\n for t in task_manager.get_tasks(limit=20, offset=0)\n if t.user_id == current_user.id\n ]\n if not recent:\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 text = (\n f\"Последняя задача: {task.id}, статус: {task.status}.\"\n + (f\"\\n{summary_line}\" if summary_line else \"\")\n )\n return (text, 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 text = (\n f\"Статус задачи {task.id}: {task.status}.\"\n + (f\"\\n{summary_line}\" if summary_line else \"\")\n )\n return (text, task.id, actions)\n\n\n# #endregion handle_get_task_status\n" + }, { "contract_id": "CleanReleaseApi", "contract_type": "Module", @@ -7545,7 +8394,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", "start_line": 1, - "end_line": 382, + "end_line": 381, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -7582,7 +8431,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dashboard, api, transform, search, task]\n# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:frontend:DashboardRouter]]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n# @RELATION DEPENDS_ON -> [DashboardHelpers]\n# @RELATION DEPENDS_ON -> [DashboardProjection]\n\nimport re\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends, HTTPException, Query, Response\nfrom fastapi.responses import JSONResponse\n\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.network import DashboardNotFoundError\nfrom src.dependencies import (\n get_config_manager,\n get_mapping_service,\n get_task_manager,\n has_permission,\n)\n\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 ._router import router\nfrom ._schemas import (\n DashboardDetailResponse,\n DashboardTaskHistoryItem,\n DashboardTaskHistoryResponse,\n DatabaseMapping,\n DatabaseMappingsResponse,\n)\n\n\n# #region get_database_mappings [C:2] [TYPE Function]\n# @BRIEF 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# @RELATION CALLS -> [EXT:method: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: {e!s}\"\n )\n\n\n# #endregion get_database_mappings\n\n\n# #region get_dashboard_detail [C:2] [TYPE Function]\n# @BRIEF 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: {e!s}\"\n )\n\n\n# #endregion get_dashboard_detail\n\n\n# #region get_dashboard_tasks_history [C:2] [TYPE Function]\n# @BRIEF 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: str | None = 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: int | None = None\n client: AsyncSupersetClient | None = 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# #endregion get_dashboard_tasks_history\n\n\n# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF 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: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n# #endregion DashboardDetailRoutes\n" + "body": "# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dashboard, api, transform, search, task]\n# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:frontend:DashboardRouter]]\n# @RELATION DEPENDS_ON -> [DashboardSchemas]\n# @RELATION DEPENDS_ON -> [DashboardHelpers]\n# @RELATION DEPENDS_ON -> [DashboardProjection]\n\nimport re\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends, HTTPException, Query, Response\nfrom fastapi.responses import JSONResponse\n\nfrom src.core.async_superset_client import AsyncSupersetClient\nfrom src.core.logger import belief_scope, logger\nfrom src.core.superset_client import SupersetClient\nfrom src.core.utils.network import DashboardNotFoundError\nfrom src.dependencies import (\n get_config_manager,\n get_mapping_service,\n get_task_manager,\n has_permission,\n)\n\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 ._router import router\nfrom ._schemas import (\n DashboardDetailResponse,\n DashboardTaskHistoryItem,\n DashboardTaskHistoryResponse,\n DatabaseMapping,\n DatabaseMappingsResponse,\n)\n\n\n# #region get_database_mappings [C:2] [TYPE Function]\n# @BRIEF 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# @RELATION CALLS -> [EXT:method: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: {e!s}\"\n )\n\n\n# #endregion get_database_mappings\n\n\n# #region get_dashboard_detail [C:2] [TYPE Function]\n# @BRIEF 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: {e!s}\"\n )\n\n\n# #endregion get_dashboard_detail\n\n\n# #region get_dashboard_tasks_history [C:2] [TYPE Function]\n# @BRIEF 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: str | None = 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: int | None = None\n client: AsyncSupersetClient | None = 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# #endregion get_dashboard_tasks_history\n\n\n# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF 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 )\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: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n# #endregion DashboardDetailRoutes\n" }, { "contract_id": "get_database_mappings", @@ -7663,7 +8512,7 @@ "contract_type": "Function", "file_path": "backend/src/api/routes/dashboards/_detail_routes.py", "start_line": 257, - "end_line": 381, + "end_line": 380, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -7683,7 +8532,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF 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: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n" + "body": "# #region get_dashboard_thumbnail [C:3] [TYPE Function]\n# @BRIEF 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 )\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: {e!s}\",\n )\n\n\n# #endregion get_dashboard_thumbnail\n" }, { "contract_id": "DashboardHelpers", @@ -8115,7 +8964,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/dashboards/_schemas.py", "start_line": 1, - "end_line": 214, + "end_line": 215, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8134,7 +8983,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS pydantic, dashboard, api, dto, git-status]\n# @BRIEF DTO classes for the Dashboard Hub API.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\n\n# #region GitStatus [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: str | None = None\n sync_status: str | None = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: bool | None = None\n has_changes_for_commit: bool | None = None\n\n\n# #endregion GitStatus\n\n\n# #region LastTask [C:2] [TYPE DataClass]\n# @BRIEF DTO for the most recent background task associated with a dashboard.\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(\n None,\n pattern=\"^PENDING|RUNNING|SUCCESS|FAILED|ERROR|AWAITING_INPUT|WAITING_INPUT|AWAITING_MAPPING$\",\n )\n validation_status: str | None = Field(None, pattern=\"^PASS|FAIL|WARN|UNKNOWN$\")\n\n\n# #endregion LastTask\n\n\n# #region DashboardItem [C:1] [TYPE DataClass]\n# @BRIEF DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n last_modified: str | None = None\n created_by: str | None = None\n modified_by: str | None = None\n owners: list[str] | None = None\n git_status: GitStatus | None = None\n last_task: LastTask | None = None\n\n\n# #endregion DashboardItem\n\n\n# #region EffectiveProfileFilter [C:1] [TYPE DataClass]\n# @BRIEF Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: str | None = None\n match_logic: Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"] | None = None\n\n\n# #endregion EffectiveProfileFilter\n\n\n# #region DashboardsResponse [C:1] [TYPE DataClass]\n# @BRIEF Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: list[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: EffectiveProfileFilter | None = None\n\n\n# #endregion DashboardsResponse\n\n\n# #region DashboardChartItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: str | None = None\n dataset_id: int | None = None\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardChartItem\n\n\n# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n\n\n# #region DashboardDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n description: str | None = None\n last_modified: str | None = None\n published: bool | None = None\n charts: list[DashboardChartItem]\n datasets: list[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# #endregion DashboardDetailResponse\n\n\n# #region DashboardTaskHistoryItem [C:1] [TYPE DataClass]\n# @BRIEF Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: str | None = None\n started_at: str | None = None\n finished_at: str | None = None\n env_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardTaskHistoryItem\n\n\n# #region DashboardTaskHistoryResponse [C:1] [TYPE DataClass]\n# @BRIEF Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: list[DashboardTaskHistoryItem]\n\n\n# #endregion DashboardTaskHistoryResponse\n\n\n# #region DatabaseMapping [C:2] [TYPE DataClass]\n# @BRIEF DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: str | None = None\n target_db_uuid: str | None = None\n confidence: float\n\n\n# #endregion DatabaseMapping\n\n\n# #region DatabaseMappingsResponse [C:1] [TYPE DataClass]\n# @BRIEF Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: list[DatabaseMapping]\n\n\n# #endregion DatabaseMappingsResponse\n\n\n# #region MigrateRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: list[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: dict[str, str] | None = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# #endregion MigrateRequest\n\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF DTO for async task ID return.\nclass TaskResponse(BaseModel):\n task_id: str\n\n\n# #endregion TaskResponse\n\n\n# #region BackupRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: list[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: str | None = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# #endregion BackupRequest\n# #endregion DashboardSchemas\n" + "body": "# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS pydantic, dashboard, api, dto, git-status]\n# @BRIEF DTO classes for the Dashboard Hub API.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom typing import Literal\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region GitStatus [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard Git synchronization status.\nclass GitStatus(BaseModel):\n branch: str | None = None\n sync_status: str | None = Field(None, pattern=\"^OK|DIFF|NO_REPO|ERROR$\")\n has_repo: bool | None = None\n has_changes_for_commit: bool | None = None\n\n\n# #endregion GitStatus\n\n\n# #region LastTask [C:2] [TYPE DataClass]\n# @BRIEF DTO for the most recent background task associated with a dashboard.\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(\n None,\n pattern=\"^PENDING|RUNNING|SUCCESS|FAILED|ERROR|AWAITING_INPUT|WAITING_INPUT|AWAITING_MAPPING$\",\n )\n validation_status: str | None = Field(None, pattern=\"^PASS|FAIL|WARN|UNKNOWN$\")\n\n\n# #endregion LastTask\n\n\n# #region DashboardItem [C:1] [TYPE DataClass]\n# @BRIEF DTO representing a single dashboard with projected metadata.\nclass DashboardItem(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n last_modified: str | None = None\n created_by: str | None = None\n modified_by: str | None = None\n owners: list[str] | None = None\n git_status: GitStatus | None = None\n last_task: LastTask | None = None\n\n\n# #endregion DashboardItem\n\n\n# #region EffectiveProfileFilter [C:1] [TYPE DataClass]\n# @BRIEF Metadata about applied profile filters for UI context.\nclass EffectiveProfileFilter(BaseModel):\n applied: bool\n source_page: Literal[\"dashboards_main\", \"other\"] = \"dashboards_main\"\n override_show_all: bool = False\n username: str | None = None\n match_logic: Literal[\"owners_or_modified_by\", \"slug_only\", \"owners_or_modified_by+slug_only\"] | None = None\n\n\n# #endregion EffectiveProfileFilter\n\n\n# #region DashboardsResponse [C:1] [TYPE DataClass]\n# @BRIEF Envelope DTO for paginated dashboards list.\nclass DashboardsResponse(BaseModel):\n dashboards: list[DashboardItem]\n total: int\n page: int\n page_size: int\n total_pages: int\n effective_profile_filter: EffectiveProfileFilter | None = None\n\n\n# #endregion DashboardsResponse\n\n\n# #region DashboardChartItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a chart linked to a dashboard.\nclass DashboardChartItem(BaseModel):\n id: int\n title: str\n viz_type: str | None = None\n dataset_id: int | None = None\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardChartItem\n\n\n# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n model_config = ConfigDict(protected_namespaces=())\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n\n\n# #region DashboardDetailResponse [C:1] [TYPE DataClass]\n# @BRIEF Detailed dashboard metadata including children.\nclass DashboardDetailResponse(BaseModel):\n id: int\n title: str\n slug: str | None = None\n url: str | None = None\n description: str | None = None\n last_modified: str | None = None\n published: bool | None = None\n charts: list[DashboardChartItem]\n datasets: list[DashboardDatasetItem]\n chart_count: int\n dataset_count: int\n\n\n# #endregion DashboardDetailResponse\n\n\n# #region DashboardTaskHistoryItem [C:1] [TYPE DataClass]\n# @BRIEF Individual history record entry.\nclass DashboardTaskHistoryItem(BaseModel):\n id: str\n plugin_id: str\n status: str\n validation_status: str | None = None\n started_at: str | None = None\n finished_at: str | None = None\n env_id: str | None = None\n summary: str | None = None\n\n\n# #endregion DashboardTaskHistoryItem\n\n\n# #region DashboardTaskHistoryResponse [C:1] [TYPE DataClass]\n# @BRIEF Collection DTO for task history.\nclass DashboardTaskHistoryResponse(BaseModel):\n dashboard_id: int\n items: list[DashboardTaskHistoryItem]\n\n\n# #endregion DashboardTaskHistoryResponse\n\n\n# #region DatabaseMapping [C:2] [TYPE DataClass]\n# @BRIEF DTO for cross-environment database ID mapping.\nclass DatabaseMapping(BaseModel):\n source_db: str\n target_db: str\n source_db_uuid: str | None = None\n target_db_uuid: str | None = None\n confidence: float\n\n\n# #endregion DatabaseMapping\n\n\n# #region DatabaseMappingsResponse [C:1] [TYPE DataClass]\n# @BRIEF Wrapper for database mappings.\nclass DatabaseMappingsResponse(BaseModel):\n mappings: list[DatabaseMapping]\n\n\n# #endregion DatabaseMappingsResponse\n\n\n# #region MigrateRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard migration requests.\nclass MigrateRequest(BaseModel):\n source_env_id: str = Field(..., description=\"Source environment ID\")\n target_env_id: str = Field(..., description=\"Target environment ID\")\n dashboard_ids: list[int] = Field(\n ..., description=\"List of dashboard IDs to migrate\"\n )\n db_mappings: dict[str, str] | None = Field(\n None, description=\"Database mappings for migration\"\n )\n replace_db_config: bool = Field(False, description=\"Replace database configuration\")\n\n\n# #endregion MigrateRequest\n\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF DTO for async task ID return.\nclass TaskResponse(BaseModel):\n task_id: str\n\n\n# #endregion TaskResponse\n\n\n# #region BackupRequest [C:1] [TYPE DataClass]\n# @BRIEF DTO for dashboard backup requests.\nclass BackupRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dashboard_ids: list[int] = Field(..., description=\"List of dashboard IDs to backup\")\n schedule: str | None = Field(\n None, description=\"Cron schedule for recurring backups (e.g., '0 0 * * *')\"\n )\n\n\n# #endregion BackupRequest\n# #endregion DashboardSchemas\n" }, { "contract_id": "GitStatus", @@ -8231,7 +9080,7 @@ "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", "start_line": 96, - "end_line": 107, + "end_line": 108, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8242,14 +9091,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n" + "body": "# #region DashboardDatasetItem [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dataset associated with a dashboard.\nclass DashboardDatasetItem(BaseModel):\n model_config = ConfigDict(protected_namespaces=())\n id: int\n table_name: str\n schema: str | None = None\n database: str\n last_modified: str | None = None\n overview: str | None = None\n\n\n# #endregion DashboardDatasetItem\n" }, { "contract_id": "DashboardDetailResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 110, - "end_line": 126, + "start_line": 111, + "end_line": 127, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8266,8 +9115,8 @@ "contract_id": "DashboardTaskHistoryItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 129, - "end_line": 142, + "start_line": 130, + "end_line": 143, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8284,8 +9133,8 @@ "contract_id": "DashboardTaskHistoryResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 145, - "end_line": 152, + "start_line": 146, + "end_line": 153, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8302,8 +9151,8 @@ "contract_id": "DatabaseMapping", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 155, - "end_line": 165, + "start_line": 156, + "end_line": 166, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -8320,8 +9169,8 @@ "contract_id": "DatabaseMappingsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 168, - "end_line": 174, + "start_line": 169, + "end_line": 175, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8338,8 +9187,8 @@ "contract_id": "MigrateRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 177, - "end_line": 191, + "start_line": 178, + "end_line": 192, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8356,8 +9205,8 @@ "contract_id": "BackupRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/dashboards/_schemas.py", - "start_line": 203, - "end_line": 213, + "start_line": 204, + "end_line": 214, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -8387,7 +9236,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewDependencies [C:2] [TYPE Module] [SEMANTICS dataset, review, dependency, di, serialize]\n# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.\n# @LAYER API\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom fastapi import Depends, Header, HTTPException, status\nfrom pydantic import BaseModel, Field\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 (\n get_config_manager,\n get_task_manager,\n)\nfrom src.models.dataset_review import (\n AnswerKind,\n ArtifactFormat,\n CandidateStatus,\n DatasetReviewSession,\n FieldProvenance,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationQuestionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationEngine,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\n\n# #region StartSessionRequest [C:1] [TYPE Class]\n# @BRIEF 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# #endregion StartSessionRequest\n\n\n# #region UpdateSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: str | None = None\n\n\n# #endregion UpdateSessionRequest\n\n\n# #region SessionCollectionResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion SessionCollectionResponse\n\n\n# #region ExportArtifactResponse [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n content: dict[str, Any]\n\n\n# #endregion ExportArtifactResponse\n\n\n# #region FieldSemanticUpdateRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n lock_field: bool = False\n resolution_note: str | None = None\n\n\n# #endregion FieldSemanticUpdateRequest\n\n\n# #region FeedbackRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# #endregion FeedbackRequest\n\n\n# #region ClarificationAnswerRequest [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n\n\n# #endregion ClarificationAnswerRequest\n\n\n# #region ClarificationSessionSummaryResponse [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n\n\n# #endregion ClarificationSessionSummaryResponse\n\n\n# #region ClarificationStateResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: ClarificationSessionSummaryResponse | None = None\n current_question: ClarificationQuestionDto | None = None\n\n\n# #endregion ClarificationStateResponse\n\n\n# #region ClarificationAnswerResultResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion ClarificationAnswerResultResponse\n\n\n# #region FeedbackResponse [C:1] [TYPE Class]\n# @BRIEF Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# #endregion FeedbackResponse\n\n\n# #region ApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: str | None = None\n\n\n# #endregion ApproveMappingRequest\n\n\n# #region BatchApproveSemanticItemRequest [C:1] [TYPE Class]\n# @BRIEF 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# #endregion BatchApproveSemanticItemRequest\n\n\n# #region BatchApproveSemanticRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: list[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# #endregion BatchApproveSemanticRequest\n\n\n# #region BatchApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: list[str] = Field(..., min_length=1)\n approval_note: str | None = None\n\n\n# #endregion BatchApproveMappingRequest\n\n\n# #region PreviewEnqueueResultResponse [C:1] [TYPE Class]\n# @BRIEF Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: int | None = None\n preview_status: str\n task_id: str | None = None\n\n\n# #endregion PreviewEnqueueResultResponse\n\n\n# #region MappingCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: list[ExecutionMappingDto]\n\n\n# #endregion MappingCollectionResponse\n\n\n# #region UpdateExecutionMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Any | None = None\n mapping_method: str | None = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: str | None = None\n\n\n# #endregion UpdateExecutionMappingRequest\n\n\n# #region LaunchDatasetResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion LaunchDatasetResponse\n\n\n# --- Dependency Injection ---\n\n# #region _require_auto_review_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_auto_review_flag\n\n\n# #region _require_clarification_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_clarification_flag\n\n\n# #region _require_execution_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_execution_flag\n\n\n# #region _get_repository [C:1] [TYPE Function]\n# @BRIEF Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# #endregion _get_repository\n\n\n# #region _get_orchestrator [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _get_orchestrator\n\n\n# #region _get_clarification_engine [C:1] [TYPE Function]\n# @BRIEF Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# #endregion _get_clarification_engine\n\n\n# --- Serialization Helpers ---\n\n# #region _serialize_session_summary [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_session_summary\n\n\n# #region _serialize_session_detail [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_session_detail\n\n\n# #region _require_session_version_header [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _require_session_version_header\n\n\n# #region _build_session_version_conflict_http_exception [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _build_session_version_conflict_http_exception\n\n\n# #region _enforce_session_version [C:3] [TYPE Function]\n# @BRIEF 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 logger.explore(\"Dataset review optimistic-lock conflict detected\", extra={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version})\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _enforce_session_version\n\n\n# #region _get_owned_session_or_404 [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_session_or_404\n\n\n# #region _require_owner_mutation_scope [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_owner_mutation_scope\n\n\n# #region _prepare_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _prepare_owned_session_mutation\n\n\n# #region _commit_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _commit_owned_session_mutation\n\n\n# #region _record_session_event [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _record_session_event\n\n\n# #region _serialize_semantic_field [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_semantic_field\n\n\n# #region _serialize_execution_mapping [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_execution_mapping\n\n\n# #region _serialize_preview [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_preview\n\n\n# #region _serialize_run_context [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_run_context\n\n\n# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_clarification_question_payload\n\n\n# #region _serialize_clarification_state [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_clarification_state\n\n\n# #region _serialize_empty_clarification_state [C:1] [TYPE Function]\n# @BRIEF Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# #endregion _serialize_empty_clarification_state\n\n\n# #region _get_latest_clarification_session_or_404 [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _get_latest_clarification_session_or_404\n\n\n# #region _get_owned_mapping_or_404 [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_mapping_or_404\n\n\n# #region _get_owned_field_or_404 [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_field_or_404\n\n\n# #region _map_candidate_provenance [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _map_candidate_provenance\n\n\n# #region _resolve_candidate_source_version [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _resolve_candidate_source_version\n\n\n# #region _update_semantic_field_state [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _update_semantic_field_state\n\n\n# #region _build_sql_lab_redirect_url [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _build_sql_lab_redirect_url\n\n\n# #region _build_documentation_export [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _build_documentation_export\n\n\n# #region _build_validation_export [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _build_validation_export\n\n\n# #endregion DatasetReviewDependencies\n" + "body": "# #region DatasetReviewDependencies [C:2] [TYPE Module] [SEMANTICS dataset, review, dependency, di, serialize]\n# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.\n# @LAYER API\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom fastapi import Depends, Header, HTTPException, status\nfrom pydantic import BaseModel, Field\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 (\n get_config_manager,\n get_task_manager,\n)\nfrom src.models.dataset_review import (\n AnswerKind,\n ArtifactFormat,\n CandidateStatus,\n DatasetReviewSession,\n FieldProvenance,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n ClarificationQuestionDto,\n CompiledPreviewDto,\n DatasetRunContextDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationEngine,\n)\nfrom src.services.dataset_review.orchestrator import (\n DatasetReviewOrchestrator,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n DatasetReviewSessionVersionConflictError,\n)\n\n\n# #region StartSessionRequest [C:1] [TYPE Class]\n# @BRIEF 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# #endregion StartSessionRequest\n\n\n# #region UpdateSessionRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for lifecycle state updates on an existing session.\nclass UpdateSessionRequest(BaseModel):\n status: SessionStatus\n note: str | None = None\n\n\n# #endregion UpdateSessionRequest\n\n\n# #region SessionCollectionResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion SessionCollectionResponse\n\n\n# #region ExportArtifactResponse [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n content: dict[str, Any]\n\n\n# #endregion ExportArtifactResponse\n\n\n# #region FieldSemanticUpdateRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for field-level semantic candidate acceptance or manual override.\nclass FieldSemanticUpdateRequest(BaseModel):\n candidate_id: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n lock_field: bool = False\n resolution_note: str | None = None\n\n\n# #endregion FieldSemanticUpdateRequest\n\n\n# #region FeedbackRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for thumbs up/down feedback.\nclass FeedbackRequest(BaseModel):\n feedback: str = Field(..., pattern=\"^(up|down)$\")\n\n\n# #endregion FeedbackRequest\n\n\n# #region ClarificationAnswerRequest [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n\n\n# #endregion ClarificationAnswerRequest\n\n\n# #region ClarificationSessionSummaryResponse [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n\n\n# #endregion ClarificationSessionSummaryResponse\n\n\n# #region ClarificationStateResponse [C:1] [TYPE Class]\n# @BRIEF Response DTO for current clarification state and active question payload.\nclass ClarificationStateResponse(BaseModel):\n clarification_session: ClarificationSessionSummaryResponse | None = None\n current_question: ClarificationQuestionDto | None = None\n\n\n# #endregion ClarificationStateResponse\n\n\n# #region ClarificationAnswerResultResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion ClarificationAnswerResultResponse\n\n\n# #region FeedbackResponse [C:1] [TYPE Class]\n# @BRIEF Minimal response DTO for persisted AI feedback actions.\nclass FeedbackResponse(BaseModel):\n target_id: str\n feedback: str\n\n\n# #endregion FeedbackResponse\n\n\n# #region ApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Optional request DTO for explicit mapping approval audit notes.\nclass ApproveMappingRequest(BaseModel):\n approval_note: str | None = None\n\n\n# #endregion ApproveMappingRequest\n\n\n# #region BatchApproveSemanticItemRequest [C:1] [TYPE Class]\n# @BRIEF 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# #endregion BatchApproveSemanticItemRequest\n\n\n# #region BatchApproveSemanticRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch semantic approvals.\nclass BatchApproveSemanticRequest(BaseModel):\n items: list[BatchApproveSemanticItemRequest] = Field(..., min_length=1)\n\n\n# #endregion BatchApproveSemanticRequest\n\n\n# #region BatchApproveMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for explicit batch mapping approvals.\nclass BatchApproveMappingRequest(BaseModel):\n mapping_ids: list[str] = Field(..., min_length=1)\n approval_note: str | None = None\n\n\n# #endregion BatchApproveMappingRequest\n\n\n# #region PreviewEnqueueResultResponse [C:1] [TYPE Class]\n# @BRIEF Async preview trigger response exposing only enqueue state.\nclass PreviewEnqueueResultResponse(BaseModel):\n session_id: str\n session_version: int | None = None\n preview_status: str\n task_id: str | None = None\n\n\n# #endregion PreviewEnqueueResultResponse\n\n\n# #region MappingCollectionResponse [C:1] [TYPE Class]\n# @BRIEF Wrapper for execution mapping list responses.\nclass MappingCollectionResponse(BaseModel):\n items: list[ExecutionMappingDto]\n\n\n# #endregion MappingCollectionResponse\n\n\n# #region UpdateExecutionMappingRequest [C:1] [TYPE Class]\n# @BRIEF Request DTO for one manual execution-mapping override update.\nclass UpdateExecutionMappingRequest(BaseModel):\n effective_value: Any | None = None\n mapping_method: str | None = Field(default=None, pattern=\"^(manual_override|direct_match|heuristic_match|semantic_match)$\")\n transformation_note: str | None = None\n\n\n# #endregion UpdateExecutionMappingRequest\n\n\n# #region LaunchDatasetResponse [C:1] [TYPE Class]\n# @BRIEF 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# #endregion LaunchDatasetResponse\n\n\n# --- Dependency Injection ---\n\n# #region _require_auto_review_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_auto_review_flag\n\n\n# #region _require_clarification_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_clarification_flag\n\n\n# #region _require_execution_flag [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_execution_flag\n\n\n# #region _get_repository [C:1] [TYPE Function]\n# @BRIEF Build repository dependency.\ndef _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:\n return DatasetReviewSessionRepository(db)\n\n\n# #endregion _get_repository\n\n\n# #region _get_orchestrator [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _get_orchestrator\n\n\n# #region _get_clarification_engine [C:1] [TYPE Function]\n# @BRIEF Build clarification engine dependency.\ndef _get_clarification_engine(\n repository: DatasetReviewSessionRepository = Depends(_get_repository),\n) -> ClarificationEngine:\n return ClarificationEngine(repository=repository)\n\n\n# #endregion _get_clarification_engine\n\n\n# --- Serialization Helpers ---\n\n# #region _serialize_session_summary [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_session_summary\n\n\n# #region _serialize_session_detail [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_session_detail\n\n\n# #region _require_session_version_header [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _require_session_version_header\n\n\n# #region _build_session_version_conflict_http_exception [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _build_session_version_conflict_http_exception\n\n\n# #region _enforce_session_version [C:3] [TYPE Function]\n# @BRIEF 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 logger.explore(\"Dataset review optimistic-lock conflict detected\", extra={\"session_id\": exc.session_id, \"expected_version\": exc.expected_version, \"actual_version\": exc.actual_version})\n raise _build_session_version_conflict_http_exception(exc) from exc\n return session\n\n\n# #endregion _enforce_session_version\n\n\n# #region _get_owned_session_or_404 [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_session_or_404\n\n\n# #region _require_owner_mutation_scope [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _require_owner_mutation_scope\n\n\n# #region _prepare_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _prepare_owned_session_mutation\n\n\n# #region _commit_owned_session_mutation [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _commit_owned_session_mutation\n\n\n# #region _record_session_event [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _record_session_event\n\n\n# #region _serialize_semantic_field [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_semantic_field\n\n\n# #region _serialize_execution_mapping [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_execution_mapping\n\n\n# #region _serialize_preview [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_preview\n\n\n# #region _serialize_run_context [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_run_context\n\n\n# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF 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.now(UTC), \"updated_at\": datetime.now(UTC),\n })\n\n\n# #endregion _serialize_clarification_question_payload\n\n\n# #region _serialize_clarification_state [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_clarification_state\n\n\n# #region _serialize_empty_clarification_state [C:1] [TYPE Function]\n# @BRIEF Return empty clarification payload.\ndef _serialize_empty_clarification_state():\n return ClarificationStateResponse(clarification_session=None, current_question=None)\n\n\n# #endregion _serialize_empty_clarification_state\n\n\n# #region _get_latest_clarification_session_or_404 [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _get_latest_clarification_session_or_404\n\n\n# #region _get_owned_mapping_or_404 [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_mapping_or_404\n\n\n# #region _get_owned_field_or_404 [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _get_owned_field_or_404\n\n\n# #region _map_candidate_provenance [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _map_candidate_provenance\n\n\n# #region _resolve_candidate_source_version [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _resolve_candidate_source_version\n\n\n# #region _update_semantic_field_state [C:3] [TYPE Function]\n# @BRIEF 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# #endregion _update_semantic_field_state\n\n\n# #region _build_sql_lab_redirect_url [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _build_sql_lab_redirect_url\n\n\n# #region _build_documentation_export [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _build_documentation_export\n\n\n# #region _build_validation_export [C:2] [TYPE Function]\n# @BRIEF 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# #endregion _build_validation_export\n\n\n# #endregion DatasetReviewDependencies\n" }, { "contract_id": "StartSessionRequest", @@ -9107,7 +9956,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF 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# #endregion _serialize_clarification_question_payload\n" + "body": "# #region _serialize_clarification_question_payload [C:1] [TYPE Function]\n# @BRIEF 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.now(UTC), \"updated_at\": datetime.now(UTC),\n })\n\n\n# #endregion _serialize_clarification_question_payload\n" }, { "contract_id": "_serialize_clarification_state", @@ -9313,7 +10162,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", "start_line": 1, - "end_line": 1099, + "end_line": 1100, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9324,14 +10173,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dataset, review, feedback, route]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, Union, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, Response, status\n\nfrom src.api.routes.dataset_review_pkg._dependencies import (\n BatchApproveMappingRequest,\n BatchApproveSemanticRequest,\n ClarificationAnswerRequest,\n ClarificationAnswerResultResponse,\n ClarificationStateResponse,\n ExportArtifactResponse,\n FeedbackRequest,\n FeedbackResponse,\n FieldSemanticUpdateRequest,\n LaunchDatasetResponse,\n MappingCollectionResponse,\n PreviewEnqueueResultResponse,\n SessionCollectionResponse,\n StartSessionRequest,\n UpdateExecutionMappingRequest,\n UpdateSessionRequest,\n _build_documentation_export,\n _build_session_version_conflict_http_exception,\n _build_sql_lab_redirect_url,\n _build_validation_export,\n _commit_owned_session_mutation,\n _get_clarification_engine,\n _get_latest_clarification_session_or_404,\n _get_orchestrator,\n _get_owned_field_or_404,\n _get_owned_mapping_or_404,\n _get_owned_session_or_404,\n _get_repository,\n _prepare_owned_session_mutation,\n _record_session_event,\n _require_auto_review_flag,\n _require_clarification_flag,\n _require_execution_flag,\n _require_session_version_header,\n _serialize_clarification_state,\n _serialize_empty_clarification_state,\n _serialize_execution_mapping,\n _serialize_preview,\n _serialize_run_context,\n _serialize_semantic_field,\n _serialize_session_detail,\n _serialize_session_summary,\n _update_semantic_field_state,\n)\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n ApprovalState,\n ArtifactFormat,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n CompiledPreviewDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionVersionConflictError,\n)\n\nrouter = APIRouter(prefix=\"/api/dataset-orchestration\", tags=[\"Dataset Orchestration\"])\n\n\n# #region list_sessions [C:3] [TYPE Function]\n# @BRIEF List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n logger.reason(\n \"Listing dataset review sessions\",\n extra={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n logger.reflect(\n \"Session page assembled\",\n extra={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# #endregion list_sessions\n\n\n# #region start_session [C:4] [TYPE Function]\n# @BRIEF Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n logger.reason(\n \"Starting dataset review session\",\n extra={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n logger.explore(\n \"Session start rejected\",\n extra={\"user_id\": current_user.id, \"error\": str(exc)},\n )\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n logger.reflect(\n \"Session started\", extra={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# #endregion start_session\n\n\n# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n\n\n# #region update_session [C:3] [TYPE Function]\n# @BRIEF Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# #endregion update_session\n\n\n# #region delete_session [C:3] [TYPE Function]\n# @BRIEF Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# #endregion delete_session\n\n\n# #region export_documentation [C:3] [TYPE Function]\n# @BRIEF Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_documentation\n\n\n# #region export_validation [C:3] [TYPE Function]\n# @BRIEF Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_validation\n\n\n# #region get_clarification_state [C:3] [TYPE Function]\n# @BRIEF Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion get_clarification_state\n\n\n# #region resume_clarification [C:3] [TYPE Function]\n# @BRIEF Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion resume_clarification\n\n\n# #region record_clarification_answer [C:3] [TYPE Function]\n# @BRIEF Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# #endregion record_clarification_answer\n\n\n# #region update_field_semantic [C:3] [TYPE Function]\n# @BRIEF Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion update_field_semantic\n\n\n# #region lock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion lock_field_semantic\n\n\n# #region unlock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion unlock_field_semantic\n\n\n# #region approve_batch_semantic_fields [C:3] [TYPE Function]\n# @BRIEF Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=list[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# #endregion approve_batch_semantic_fields\n\n\n# #region list_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# #endregion list_execution_mappings\n\n\n# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n\n\n# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n\n\n# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n\n\n# #region trigger_preview_generation [C:3] [TYPE Function]\n# @BRIEF Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# #endregion trigger_preview_generation\n\n\n# #region launch_dataset [C:3] [TYPE Function]\n# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# #endregion launch_dataset\n\n\n# #region record_field_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# #endregion record_field_feedback\n\n\n# #region record_clarification_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# #endregion record_clarification_feedback\n\n\n# #endregion DatasetReviewRoutes\n" + "body": "# #region DatasetReviewRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dataset, review, feedback, route]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any, Union, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, Response, status\n\nfrom src.api.routes.dataset_review_pkg._dependencies import (\n BatchApproveMappingRequest,\n BatchApproveSemanticRequest,\n ClarificationAnswerRequest,\n ClarificationAnswerResultResponse,\n ClarificationStateResponse,\n ExportArtifactResponse,\n FeedbackRequest,\n FeedbackResponse,\n FieldSemanticUpdateRequest,\n LaunchDatasetResponse,\n MappingCollectionResponse,\n PreviewEnqueueResultResponse,\n SessionCollectionResponse,\n StartSessionRequest,\n UpdateExecutionMappingRequest,\n UpdateSessionRequest,\n _build_documentation_export,\n _build_session_version_conflict_http_exception,\n _build_sql_lab_redirect_url,\n _build_validation_export,\n _commit_owned_session_mutation,\n _get_clarification_engine,\n _get_latest_clarification_session_or_404,\n _get_orchestrator,\n _get_owned_field_or_404,\n _get_owned_mapping_or_404,\n _get_owned_session_or_404,\n _get_repository,\n _prepare_owned_session_mutation,\n _record_session_event,\n _require_auto_review_flag,\n _require_clarification_flag,\n _require_execution_flag,\n _require_session_version_header,\n _serialize_clarification_state,\n _serialize_empty_clarification_state,\n _serialize_execution_mapping,\n _serialize_preview,\n _serialize_run_context,\n _serialize_semantic_field,\n _serialize_session_detail,\n _serialize_session_summary,\n _update_semantic_field_state,\n)\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n ApprovalState,\n ArtifactFormat,\n FieldProvenance,\n MappingMethod,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n SessionStatus,\n)\nfrom src.schemas.dataset_review import (\n CompiledPreviewDto,\n ExecutionMappingDto,\n SemanticFieldEntryDto,\n SessionDetail,\n SessionSummary,\n ValidationFindingDto,\n)\nfrom src.services.dataset_review.clarification_engine import (\n ClarificationAnswerCommand,\n ClarificationStateResult,\n)\nfrom src.services.dataset_review.orchestrator import (\n LaunchDatasetCommand,\n PreparePreviewCommand,\n StartSessionCommand,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionVersionConflictError,\n)\n\nrouter = APIRouter(prefix=\"/api/dataset-orchestration\", tags=[\"Dataset Orchestration\"])\n\n\n# #region list_sessions [C:3] [TYPE Function]\n# @BRIEF List resumable dataset review sessions for the current user.\n@router.get(\n \"/sessions\",\n response_model=SessionCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_sessions(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_sessions\"):\n logger.reason(\n \"Listing dataset review sessions\",\n extra={\"user_id\": current_user.id, \"page\": page, \"page_size\": page_size},\n )\n sessions = repository.list_sessions_for_user(current_user.id)\n start = (page - 1) * page_size\n end = start + page_size\n items = [_serialize_session_summary(s) for s in sessions[start:end]]\n logger.reflect(\n \"Session page assembled\",\n extra={\n \"user_id\": current_user.id,\n \"returned\": len(items),\n \"total\": len(sessions),\n },\n )\n return SessionCollectionResponse(\n items=items,\n total=len(sessions),\n page=page,\n page_size=page_size,\n has_next=end < len(sessions),\n )\n\n\n# #endregion list_sessions\n\n\n# #region start_session [C:4] [TYPE Function]\n# @BRIEF Start a new dataset review session from a Superset link or dataset selection.\n@router.post(\n \"/sessions\",\n response_model=SessionSummary,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def start_session(\n request: StartSessionRequest,\n orchestrator=Depends(_get_orchestrator),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"start_session\"):\n logger.reason(\n \"Starting dataset review session\",\n extra={\n \"user_id\": current_user.id,\n \"environment_id\": request.environment_id,\n },\n )\n try:\n result = orchestrator.start_session(\n StartSessionCommand(\n user=current_user,\n environment_id=request.environment_id,\n source_kind=request.source_kind,\n source_input=request.source_input,\n )\n )\n except ValueError as exc:\n logger.explore(\n \"Session start rejected\",\n extra={\"user_id\": current_user.id, \"error\": str(exc)},\n )\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail == \"Environment not found\"\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n logger.reflect(\n \"Session started\", extra={\"session_id\": result.session.session_id}\n )\n return _serialize_session_summary(result.session)\n\n\n# #endregion start_session\n\n\n# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionDetail,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n\n\n# #region update_session [C:3] [TYPE Function]\n# @BRIEF Update resumable lifecycle status for an owned session.\n@router.patch(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_session(\n session_id: str,\n request: UpdateSessionRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"update_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n session_record = cast(Any, session)\n session_record.status = request.status\n if request.status == SessionStatus.PAUSED:\n session_record.recommended_action = RecommendedAction.RESUME_SESSION\n elif request.status in {\n SessionStatus.ARCHIVED,\n SessionStatus.CANCELLED,\n SessionStatus.COMPLETED,\n }:\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_status_updated\",\n event_summary=\"Dataset review session lifecycle updated\",\n event_details={\n \"status\": session_record.status.value,\n \"version\": session_record.version,\n },\n )\n return _serialize_session_summary(session)\n\n\n# #endregion update_session\n\n\n# #region delete_session [C:3] [TYPE Function]\n# @BRIEF Archive or hard-delete a session owned by the current user.\n@router.delete(\n \"/sessions/{session_id}\",\n status_code=status.HTTP_204_NO_CONTENT,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def delete_session(\n session_id: str,\n hard_delete: bool = Query(False),\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"delete_session\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n if hard_delete:\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_deleted\",\n event_summary=\"Session hard-deleted\",\n event_details={\"hard_delete\": True},\n )\n repository.db.delete(session)\n repository.db.commit()\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n session_record = cast(Any, session)\n session_record.status = SessionStatus.ARCHIVED\n session_record.active_task_id = None\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"session_archived\",\n event_summary=\"Session archived\",\n event_details={\"hard_delete\": False, \"version\": session_record.version},\n )\n return Response(status_code=status.HTTP_204_NO_CONTENT)\n\n\n# #endregion delete_session\n\n\n# #region export_documentation [C:3] [TYPE Function]\n# @BRIEF Export documentation output for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/documentation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_documentation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_documentation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_documentation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"documentation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"documentation\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_documentation\n\n\n# #region export_validation [C:3] [TYPE Function]\n# @BRIEF Export validation findings for the current session.\n@router.get(\n \"/sessions/{session_id}/exports/validation\",\n response_model=ExportArtifactResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def export_validation(\n session_id: str,\n format: ArtifactFormat = Query(ArtifactFormat.JSON),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"export_validation\"):\n if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Only json and markdown exports are supported\",\n )\n session = _get_owned_session_or_404(repository, session_id, current_user)\n payload = _build_validation_export(session, format)\n return ExportArtifactResponse(\n artifact_id=f\"validation-{session.session_id}-{format.value}\",\n session_id=session.session_id,\n artifact_type=\"validation_report\",\n format=format.value,\n storage_ref=payload[\"storage_ref\"],\n created_by_user_id=current_user.id,\n content=payload[\"content\"],\n )\n\n\n# #endregion export_validation\n\n\n# #region get_clarification_state [C:3] [TYPE Function]\n# @BRIEF Return the current clarification session summary and active question payload.\n@router.get(\n \"/sessions/{session_id}/clarification\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_clarification_state(\n session_id: str,\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"get_clarification_state\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n if not session.clarification_sessions:\n return _serialize_empty_clarification_state()\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion get_clarification_state\n\n\n# #region resume_clarification [C:3] [TYPE Function]\n# @BRIEF Resume clarification mode on the highest-priority unresolved question.\n@router.post(\n \"/sessions/{session_id}/clarification/resume\",\n response_model=ClarificationStateResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def resume_clarification(\n session_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"resume_clarification\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = clarification_engine.build_question_payload(session)\n return _serialize_clarification_state(\n ClarificationStateResult(\n clarification_session=cs,\n current_question=question,\n session=session,\n changed_findings=[],\n )\n )\n\n\n# #endregion resume_clarification\n\n\n# #region record_clarification_answer [C:3] [TYPE Function]\n# @BRIEF Persist one clarification answer before advancing the active pointer.\n@router.post(\n \"/sessions/{session_id}/clarification/answers\",\n response_model=ClarificationAnswerResultResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_answer(\n session_id: str,\n request: ClarificationAnswerRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n clarification_engine=Depends(_get_clarification_engine),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_answer\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = clarification_engine.record_answer(\n ClarificationAnswerCommand(\n session=session,\n question_id=request.question_id,\n answer_kind=request.answer_kind,\n answer_value=request.answer_value,\n user=current_user,\n )\n )\n except ValueError as exc:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)\n ) from exc\n return ClarificationAnswerResultResponse(\n clarification_state=_serialize_clarification_state(result),\n session=_serialize_session_summary(result.session),\n changed_findings=[\n ValidationFindingDto.model_validate(f, from_attributes=True)\n for f in result.changed_findings\n ],\n )\n\n\n# #endregion record_clarification_answer\n\n\n# #region update_field_semantic [C:3] [TYPE Function]\n# @BRIEF Apply one field-level semantic candidate decision or manual override.\n@router.patch(\n \"/sessions/{session_id}/fields/{field_id}/semantic\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_field_semantic(\n session_id: str,\n field_id: str,\n request: FieldSemanticUpdateRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n _update_semantic_field_state(field, request, changed_by=\"user\")\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_updated\",\n event_summary=\"Semantic field decision persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion update_field_semantic\n\n\n# #region lock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Lock one semantic field against later automatic overwrite.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/lock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def lock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.lock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = True\n field.last_changed_by = \"user\"\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_locked\",\n event_summary=\"Semantic field lock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion lock_field_semantic\n\n\n# #region unlock_field_semantic [C:3] [TYPE Function]\n# @BRIEF Unlock one semantic field so later automated candidate application may replace it.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/unlock\",\n response_model=SemanticFieldEntryDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def unlock_field_semantic(\n session_id: str,\n field_id: str,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.unlock_field_semantic\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.is_locked = False\n field.last_changed_by = \"user\"\n if field.provenance == FieldProvenance.MANUAL_OVERRIDE:\n field.provenance = FieldProvenance.UNRESOLVED\n field.needs_review = True\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[field])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_unlocked\",\n event_summary=\"Semantic field unlock persisted\",\n event_details={\"field_id\": field.field_id, \"version\": sr.version},\n )\n return _serialize_semantic_field(field)\n\n\n# #endregion unlock_field_semantic\n\n\n# #region approve_batch_semantic_fields [C:3] [TYPE Function]\n# @BRIEF Approve multiple semantic candidate decisions in one batch.\n@router.post(\n \"/sessions/{session_id}/fields/semantic/approve-batch\",\n response_model=list[SemanticFieldEntryDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_semantic_fields(\n session_id: str,\n request: BatchApproveSemanticRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_semantic_fields\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for item in request.items:\n field = _get_owned_field_or_404(session, item.field_id)\n _update_semantic_field_state(\n field,\n FieldSemanticUpdateRequest(\n candidate_id=item.candidate_id, lock_field=item.lock_field\n ),\n changed_by=\"user\",\n )\n updated.append(field)\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_fields_batch_approved\",\n event_summary=\"Batch semantic approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_semantic_field(f) for f in updated]\n\n\n# #endregion approve_batch_semantic_fields\n\n\n# #region list_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Return the current mapping-review set for one accessible session.\n@router.get(\n \"/sessions/{session_id}/mappings\",\n response_model=MappingCollectionResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def list_execution_mappings(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.list_execution_mappings\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return MappingCollectionResponse(\n items=[_serialize_execution_mapping(m) for m in session.execution_mappings]\n )\n\n\n# #endregion list_execution_mappings\n\n\n# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n session.last_activity_at = datetime.now(UTC)\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n\n\n# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n\n\n# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n\n\n# #region trigger_preview_generation [C:3] [TYPE Function]\n# @BRIEF Trigger Superset-side preview compilation for the current owned execution context.\n@router.post(\n \"/sessions/{session_id}/preview\",\n response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def trigger_preview_generation(\n session_id: str,\n response: Response,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.trigger_preview_generation\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.prepare_launch_preview(\n PreparePreviewCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Preview blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n if result.preview.preview_status == PreviewStatus.PENDING:\n response.status_code = status.HTTP_202_ACCEPTED\n return PreviewEnqueueResultResponse(\n session_id=result.session.session_id,\n session_version=int(getattr(result.session, \"version\", 0) or 0),\n preview_status=result.preview.preview_status.value,\n task_id=None,\n )\n response.status_code = status.HTTP_200_OK\n return _serialize_preview(\n result.preview,\n session_version_fallback=int(getattr(result.session, \"version\", 0) or 0),\n )\n\n\n# #endregion trigger_preview_generation\n\n\n# #region launch_dataset [C:3] [TYPE Function]\n# @BRIEF Execute the current owned session launch handoff and return audited SQL Lab run context.\n@router.post(\n \"/sessions/{session_id}/launch\",\n response_model=LaunchDatasetResponse,\n status_code=status.HTTP_201_CREATED,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:execution:launch\", \"EXECUTE\")),\n ],\n)\nasync def launch_dataset(\n session_id: str,\n orchestrator=Depends(_get_orchestrator),\n repository=Depends(_get_repository),\n session_version: int = Depends(_require_session_version_header),\n config_manager=Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.launch_dataset\"):\n _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n try:\n result = orchestrator.launch_dataset(\n LaunchDatasetCommand(\n user=current_user,\n session_id=session_id,\n expected_version=session_version,\n )\n )\n except DatasetReviewSessionVersionConflictError as exc:\n raise _build_session_version_conflict_http_exception(exc) from exc\n except ValueError as exc:\n detail = str(exc)\n sc = (\n status.HTTP_404_NOT_FOUND\n if detail in {\"Session not found\", \"Environment not found\"}\n else status.HTTP_409_CONFLICT\n if detail.startswith(\"Launch blocked:\")\n else status.HTTP_400_BAD_REQUEST\n )\n raise HTTPException(status_code=sc, detail=detail) from exc\n environment = config_manager.get_environment(result.session.environment_id)\n env_url = getattr(environment, \"url\", \"\") if environment is not None else \"\"\n return LaunchDatasetResponse(\n run_context=_serialize_run_context(result.run_context),\n redirect_url=_build_sql_lab_redirect_url(\n environment_url=env_url,\n sql_lab_session_ref=result.run_context.sql_lab_session_ref,\n ),\n )\n\n\n# #endregion launch_dataset\n\n\n# #region record_field_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for AI-assisted semantic field content.\n@router.post(\n \"/sessions/{session_id}/fields/{field_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_field_feedback(\n session_id: str,\n field_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_field_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n field = _get_owned_field_or_404(session, field_id)\n field.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"semantic_field_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"field_id\": field.field_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)\n\n\n# #endregion record_field_feedback\n\n\n# #region record_clarification_feedback [C:3] [TYPE Function]\n# @BRIEF Persist thumbs up/down feedback for clarification question/answer content.\n@router.post(\n \"/sessions/{session_id}/clarification/questions/{question_id}/feedback\",\n response_model=FeedbackResponse,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_clarification_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def record_clarification_feedback(\n session_id: str,\n question_id: str,\n request: FeedbackRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.record_clarification_feedback\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n cs = _get_latest_clarification_session_or_404(session)\n question = next((q for q in cs.questions if q.question_id == question_id), None)\n if question is None:\n raise HTTPException(\n status_code=status.HTTP_404_NOT_FOUND,\n detail=\"Clarification question not found\",\n )\n if question.answer is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"Clarification answer not found\",\n )\n question.answer.user_feedback = request.feedback\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session)\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"clarification_feedback_recorded\",\n event_summary=\"Feedback persisted\",\n event_details={\n \"question_id\": question.question_id,\n \"feedback\": request.feedback,\n \"version\": sr.version,\n },\n )\n return FeedbackResponse(\n target_id=question.question_id, feedback=request.feedback\n )\n\n\n# #endregion record_clarification_feedback\n\n\n# #endregion DatasetReviewRoutes\n" }, { "contract_id": "list_sessions", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 93, - "end_line": 135, + "start_line": 94, + "end_line": 136, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9348,8 +10197,8 @@ "contract_id": "start_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 138, - "end_line": 189, + "start_line": 139, + "end_line": 190, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -9366,8 +10215,8 @@ "contract_id": "get_session_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 192, - "end_line": 212, + "start_line": 193, + "end_line": 213, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9378,14 +10227,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionSummary,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n" + "body": "# #region get_session_detail [C:3] [TYPE Function]\n# @BRIEF Return the full accessible dataset review session aggregate.\n@router.get(\n \"/sessions/{session_id}\",\n response_model=SessionDetail,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(has_permission(\"dataset:session\", \"READ\")),\n ],\n)\nasync def get_session_detail(\n session_id: str,\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.get_session_detail\"):\n session = _get_owned_session_or_404(repository, session_id, current_user)\n return _serialize_session_detail(session)\n\n\n# #endregion get_session_detail\n" }, { "contract_id": "update_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 215, - "end_line": 261, + "start_line": 216, + "end_line": 262, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9402,8 +10251,8 @@ "contract_id": "delete_session", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 264, - "end_line": 312, + "start_line": 265, + "end_line": 313, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9420,8 +10269,8 @@ "contract_id": "export_documentation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 315, - "end_line": 350, + "start_line": 316, + "end_line": 351, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9438,8 +10287,8 @@ "contract_id": "export_validation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 353, - "end_line": 388, + "start_line": 354, + "end_line": 389, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9456,8 +10305,8 @@ "contract_id": "get_clarification_state", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 391, - "end_line": 424, + "start_line": 392, + "end_line": 425, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9474,8 +10323,8 @@ "contract_id": "resume_clarification", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 427, - "end_line": 461, + "start_line": 428, + "end_line": 462, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9492,8 +10341,8 @@ "contract_id": "record_clarification_answer", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 464, - "end_line": 511, + "start_line": 465, + "end_line": 512, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9510,8 +10359,8 @@ "contract_id": "update_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 514, - "end_line": 551, + "start_line": 515, + "end_line": 552, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9528,8 +10377,8 @@ "contract_id": "lock_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 554, - "end_line": 591, + "start_line": 555, + "end_line": 592, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9546,8 +10395,8 @@ "contract_id": "unlock_field_semantic", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 594, - "end_line": 634, + "start_line": 595, + "end_line": 635, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9564,8 +10413,8 @@ "contract_id": "approve_batch_semantic_fields", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 637, - "end_line": 684, + "start_line": 638, + "end_line": 685, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9582,8 +10431,8 @@ "contract_id": "list_execution_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 687, - "end_line": 710, + "start_line": 688, + "end_line": 711, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9600,8 +10449,8 @@ "contract_id": "update_execution_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 713, - "end_line": 775, + "start_line": 714, + "end_line": 776, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9612,14 +10461,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n session.last_activity_at = datetime.utcnow()\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n" + "body": "# #region update_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Persist one owner-authorized execution-mapping effective value override.\n@router.patch(\n \"/sessions/{session_id}/mappings/{mapping_id}\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def update_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: UpdateExecutionMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.update_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n if request.effective_value is None:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=\"effective_value is required\",\n )\n mapping.effective_value = request.effective_value\n mapping.mapping_method = MappingMethod(\n request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value\n )\n mapping.transformation_note = request.transformation_note\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n session.last_activity_at = datetime.now(UTC)\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n if session.readiness_state in {\n ReadinessState.MAPPING_REVIEW_NEEDED,\n ReadinessState.COMPILED_PREVIEW_READY,\n ReadinessState.RUN_READY,\n ReadinessState.RUN_IN_PROGRESS,\n }:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n for preview in session.previews:\n if preview.preview_status == PreviewStatus.READY:\n preview.preview_status = PreviewStatus.STALE\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_updated\",\n event_summary=\"Mapping override persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion update_execution_mapping\n" }, { "contract_id": "approve_execution_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 778, - "end_line": 823, + "start_line": 779, + "end_line": 824, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9630,14 +10479,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n" + "body": "# #region approve_execution_mapping [C:3] [TYPE Function]\n# @BRIEF Explicitly approve a warning-sensitive mapping transformation.\n@router.post(\n \"/sessions/{session_id}/mappings/{mapping_id}/approve\",\n response_model=ExecutionMappingDto,\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_execution_mapping(\n session_id: str,\n mapping_id: str,\n request: ApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_execution_mapping\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n mapping = _get_owned_mapping_or_404(session, mapping_id)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(repository, session, refresh_targets=[mapping])\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mapping_approved\",\n event_summary=\"Mapping approval persisted\",\n event_details={\"mapping_id\": mapping.mapping_id, \"version\": sr.version},\n )\n return _serialize_execution_mapping(mapping)\n\n\n# #endregion approve_execution_mapping\n" }, { "contract_id": "approve_batch_execution_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 826, - "end_line": 875, + "start_line": 827, + "end_line": 876, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9648,14 +10497,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.utcnow()\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.utcnow()\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n" + "body": "# #region approve_batch_execution_mappings [C:3] [TYPE Function]\n# @BRIEF Approve multiple warning-sensitive execution mappings in one batch.\n@router.post(\n \"/sessions/{session_id}/mappings/approve-batch\",\n response_model=list[ExecutionMappingDto],\n dependencies=[\n Depends(_require_auto_review_flag),\n Depends(_require_execution_flag),\n Depends(has_permission(\"dataset:session\", \"MANAGE\")),\n ],\n)\nasync def approve_batch_execution_mappings(\n session_id: str,\n request: BatchApproveMappingRequest,\n session_version: int = Depends(_require_session_version_header),\n repository=Depends(_get_repository),\n current_user: User = Depends(get_current_user),\n):\n with belief_scope(\"dataset_review.approve_batch_execution_mappings\"):\n session = _prepare_owned_session_mutation(\n repository, session_id, current_user, session_version\n )\n updated = []\n for mid in list(dict.fromkeys(request.mapping_ids)):\n mapping = _get_owned_mapping_or_404(session, mid)\n mapping.approval_state = ApprovalState.APPROVED\n mapping.approved_by_user_id = current_user.id\n mapping.approved_at = datetime.now(UTC)\n if request.approval_note:\n mapping.transformation_note = request.approval_note\n updated.append(mapping)\n session.last_activity_at = datetime.now(UTC)\n if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n sr = cast(Any, session)\n _commit_owned_session_mutation(\n repository, session, refresh_targets=list(updated)\n )\n _record_session_event(\n repository,\n session,\n current_user,\n event_type=\"execution_mappings_batch_approved\",\n event_summary=\"Batch mapping approval persisted\",\n event_details={\"count\": len(updated), \"version\": sr.version},\n )\n return [_serialize_execution_mapping(m) for m in updated]\n\n\n# #endregion approve_batch_execution_mappings\n" }, { "contract_id": "trigger_preview_generation", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 878, - "end_line": 936, + "start_line": 879, + "end_line": 937, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9672,8 +10521,8 @@ "contract_id": "launch_dataset", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 939, - "end_line": 994, + "start_line": 940, + "end_line": 995, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9690,8 +10539,8 @@ "contract_id": "record_field_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 997, - "end_line": 1038, + "start_line": 998, + "end_line": 1039, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9708,8 +10557,8 @@ "contract_id": "record_clarification_feedback", "contract_type": "Function", "file_path": "backend/src/api/routes/dataset_review_pkg/_routes.py", - "start_line": 1041, - "end_line": 1096, + "start_line": 1042, + "end_line": 1097, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -9727,7 +10576,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/datasets.py", "start_line": 1, - "end_line": 681, + "end_line": 679, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -9763,7 +10612,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]\n#\n# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ResourceService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @PRE SupersetClient is available; env_id is valid.\n# @POST Returns dataset metadata with mapping status.\n# @SIDE_EFFECT Reads from Superset API and task manager.\n# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse\n# @INVARIANT All dataset responses include last_task metadata\n\n\nimport re\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom pydantic import BaseModel, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission\n\nrouter = APIRouter(prefix=\"/api/datasets\", tags=[\"Datasets\"])\n\n# #region MappedFields [C:1] [TYPE DataClass]\n# @BRIEF DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# #endregion MappedFields\n\n# #region LastTask [C:1] [TYPE DataClass]\n# @BRIEF DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# #endregion LastTask\n\n# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n metric_count: int = 0\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetItem\n\n# #region LinkedDashboard [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: str | None = None\n# #endregion LinkedDashboard\n\n# #region DatasetColumn [C:1] [TYPE DataClass]\n# @BRIEF DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: str | None = None\n is_dttm: bool = False\n is_active: bool = True\n description: str | None = None\n# #endregion DatasetColumn\n\n# #region MetricItem [C:1] [TYPE DataClass]\n# @BRIEF Pydantic DTO for a dataset metric — carries Superset metric metadata.\n# @RELATION DEPENDS_ON -> [DatasetDetailResponse]\nclass MetricItem(BaseModel):\n id: int\n metric_name: str\n expression: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n metric_type: str | None = None\n# #endregion MetricItem\n\n# #region DatasetDetailResponse [C:2] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.\n# @RELATION DEPENDS_ON -> [MetricItem]\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n metrics: list[MetricItem] = []\n metric_count: int = 0\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetDetailResponse\n\n# #region StatsCounts [C:1] [TYPE DataClass]\n# @BRIEF Aggregate statistics for the Stats Bar — computed from full dataset list.\nclass StatsCounts(BaseModel):\n total: int\n unmapped_count: int\n mapped_count: int\n linked_count: int\n# #endregion StatsCounts\n\n# #region DatasetsResponse [C:2] [TYPE DataClass]\n# @BRIEF Paginated response DTO for dataset listings with stats.\n# @RELATION DEPENDS_ON -> [StatsCounts]\nclass DatasetsResponse(BaseModel):\n datasets: list[DatasetItem]\n stats: StatsCounts\n total: int\n page: int\n page_size: int\n total_pages: int\n# #endregion DatasetsResponse\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# #endregion TaskResponse\n\n# #region ColumnDescriptionUpdate [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for inline-edit of a column description.\nclass ColumnDescriptionUpdate(BaseModel):\n description: str\n# #endregion ColumnDescriptionUpdate\n\n# #region MetricDescriptionUpdate [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for inline-edit of a metric description.\nclass MetricDescriptionUpdate(BaseModel):\n description: str\n# #endregion MetricDescriptionUpdate\n\n# #region get_dataset_ids [C:4] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE env_id must be a valid environment ID\n# @POST Returns a list of all dataset IDs\n# @RELATION CALLS -> [EXT:method:get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\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(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n\n# #region get_datasets [C:4] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress and stats.\n# @PRE env_id must be a valid environment ID\n# @PRE page must be >= 1 if provided\n# @PRE page_size must be between 1 and 100 if provided\n# @POST Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.\n# @POST Response includes pagination metadata (page, page_size, total, total_pages) and stats object.\n# @RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).\n# @RELATION CALLS -> [EXT:method:get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n filter: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, filter={filter}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\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(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Compute stats from full list BEFORE filtering/pagination\n total_datasets = len(datasets)\n unmapped = sum(\n 1 for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) == 0\n )\n mapped = sum(\n 1 for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) > 0\n )\n linked = sum(\n 1 for d in datasets\n if d.get(\"linked_dashboard_count\", 0) > 0\n )\n stats = StatsCounts(\n total=total_datasets,\n unmapped_count=unmapped,\n mapped_count=mapped,\n linked_count=linked,\n )\n\n # Apply server-side filter (before pagination) — FR-026\n if filter and filter in (\"unmapped\", \"mapped\", \"linked\"):\n if filter == \"unmapped\":\n datasets = [\n d for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) == 0\n ]\n elif filter == \"mapped\":\n datasets = [\n d for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) > 0\n ]\n elif filter == \"linked\":\n datasets = [\n d for d in datasets\n if d.get(\"linked_dashboard_count\", 0) > 0\n ]\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total}, stats: total={stats.total} unmapped={stats.unmapped_count} mapped={stats.mapped_count} linked={stats.linked_count})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n stats=stats,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n\n# #region MapColumnsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating column mapping\nclass MapColumnsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to map\")\n source_type: str = Field(..., description=\"Source type: 'sqllab' or 'xlsx'\")\n database_id: int | None = Field(None, description=\"Superset database ID for SQL Lab source\")\n sql_query: str | None = Field(None, description=\"Custom SQL query for SQL Lab (optional, defaults to information_schema.columns query)\")\n file_data: str | None = Field(None, description=\"File path or data for XLSX source\")\n# #endregion MapColumnsRequest\n\n# #region map_columns [C:4] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE User has permission plugin:mapper:execute\n# @PRE env_id is a valid environment ID\n# @PRE dataset_ids is a non-empty list\n# @POST Returns task_id for tracking mapping progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['sqllab', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'sqllab' or 'xlsx'\")\n\n # Validate database_id for sqllab source\n if request.source_type == 'sqllab' and not request.database_id:\n raise HTTPException(status_code=400, detail=\"database_id is required for 'sqllab' source type\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': 'sqllab' if request.source_type == 'sqllab' else 'excel',\n 'database_id': request.database_id,\n 'sql_query': request.sql_query,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n\n# #region GenerateDocsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating documentation generation\nclass GenerateDocsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to generate docs for\")\n llm_provider: str = Field(..., description=\"LLM provider to use\")\n options: dict | None = Field(None, description=\"Additional options for documentation generation\")\n# #endregion GenerateDocsRequest\n\n# #region generate_docs [C:4] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE User has permission plugin:llm_analysis:execute\n# @PRE env_id is a valid environment ID\n# @PRE dataset_ids is a non-empty list\n# @POST Returns task_id for tracking documentation generation progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n\n# #region get_dataset_detail [C:4] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE env_id is a valid environment ID\n# @PRE dataset_id is a valid dataset ID\n# @POST Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\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(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Normalize 'database' field: Superset returns an object, model expects a string\n if isinstance(dataset_detail.get(\"database\"), dict):\n db_obj = dataset_detail[\"database\"]\n dataset_detail[\"database\"] = db_obj.get(\"database_name\", str(db_obj.get(\"id\", \"\")))\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n\n# #region _strip_html_tags [C:2] [TYPE Function] [SEMANTICS helper,validation,html]\n# @BRIEF Detect and strip HTML tags from a text string. Uses simple regex <[^>]*>.\n# If HTML is detected, strips tags and logs a warning. Returns cleaned text.\n# @REJECTED Reject-with-400 approach was rejected in favour of silent stripping — more user-friendly\n# and matches the UX expectation that inline-edited descriptions are plain text only.\ndef _strip_html_tags(text: str) -> str:\n if not text:\n return text\n if re.search(r\"<[^>]*>\", text):\n cleaned = re.sub(r\"<[^>]*>\", \"\", text)\n logger.warning(f\"[_strip_html_tags] HTML tags detected and stripped from description\")\n return cleaned\n return text\n# #endregion _strip_html_tags\n\n# #region update_column_description [C:4] [TYPE Endpoint]\n# @BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.\n# @PRE dataset_id and column_id must exist in the target environment.\n# @POST Column description in Superset is updated. Response confirms success.\n# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.\n# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or column not found. 502 if Superset upstream fails.\n# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]\n# @RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with override_columns=false.\n# @REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.\n@router.put(\"/{dataset_id}/columns/{column_id}/description\")\nasync def update_column_description(\n dataset_id: int,\n column_id: int,\n env_id: str = Query(..., description=\"Environment ID\"),\n body: ColumnDescriptionUpdate = ...,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"WRITE\"))\n):\n with belief_scope(\"update_column_description\", f\"dataset_id={dataset_id}, column_id={column_id}, env_id={env_id}\"):\n # Validate and sanitize description\n description = _strip_html_tags(body.description)\n if len(description) > 2000:\n logger.error(f\"[update_column_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}\")\n raise HTTPException(status_code=400, detail=\"Description must not exceed 2000 characters\")\n\n # Validate environment\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(f\"[update_column_description][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n\n # GET full dataset\n logger.reason(\"Fetching full dataset from Superset for column description update\",\n extra={\"src\": \"update_column_description\"})\n try:\n response = client.get_dataset(dataset_id)\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Superset GET failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to fetch dataset from Superset: {e!s}\")\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n if not dataset or not dataset.get(\"id\"):\n logger.error(f\"[update_column_description][Coherence:Failed] Dataset {dataset_id} not found\")\n raise HTTPException(status_code=404, detail=f\"Dataset {dataset_id} not found\")\n\n # Find and modify the target column\n columns = dataset.get(\"columns\", [])\n found = False\n for col in columns:\n if col.get(\"id\") == column_id:\n col[\"description\"] = description\n found = True\n break\n\n if not found:\n logger.error(f\"[update_column_description][Coherence:Failed] Column {column_id} not found in dataset {dataset_id}\")\n raise HTTPException(status_code=404, detail=f\"Column {column_id} not found in dataset {dataset_id}\")\n\n # PUT back with override_columns=false\n put_payload = dataset\n try:\n client.update_dataset(dataset_id, put_payload, override_columns=False)\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Superset PUT failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to update dataset in Superset: {e!s}\")\n\n logger.reflect(\n f\"Updated column {column_id} description for dataset {dataset_id}\",\n extra={\"src\": \"update_column_description\"},\n )\n return {\"success\": True, \"description\": description}\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Unexpected error: {e}\")\n raise HTTPException(status_code=500, detail=f\"Unexpected error: {e!s}\")\n# #endregion update_column_description\n\n# #region update_metric_description [C:4] [TYPE Endpoint]\n# @BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.\n# @PRE dataset_id and metric_id must exist in the target environment.\n# @POST Metric description in Superset is updated.\n# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.\n# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or metric not found. 502 if Superset upstream fails.\n# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]\n@router.put(\"/{dataset_id}/metrics/{metric_id}/description\")\nasync def update_metric_description(\n dataset_id: int,\n metric_id: int,\n env_id: str = Query(..., description=\"Environment ID\"),\n body: MetricDescriptionUpdate = ...,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"WRITE\"))\n):\n with belief_scope(\"update_metric_description\", f\"dataset_id={dataset_id}, metric_id={metric_id}, env_id={env_id}\"):\n # Validate and sanitize description\n description = _strip_html_tags(body.description)\n if len(description) > 2000:\n logger.error(f\"[update_metric_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}\")\n raise HTTPException(status_code=400, detail=\"Description must not exceed 2000 characters\")\n\n # Validate environment\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(f\"[update_metric_description][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n\n # GET full dataset\n logger.reason(\"Fetching full dataset from Superset for metric description update\",\n extra={\"src\": \"update_metric_description\"})\n try:\n response = client.get_dataset(dataset_id)\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Superset GET failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to fetch dataset from Superset: {e!s}\")\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n if not dataset or not dataset.get(\"id\"):\n logger.error(f\"[update_metric_description][Coherence:Failed] Dataset {dataset_id} not found\")\n raise HTTPException(status_code=404, detail=f\"Dataset {dataset_id} not found\")\n\n # Find and modify the target metric\n metrics = dataset.get(\"metrics\", [])\n found = False\n for metric in metrics:\n if metric.get(\"id\") == metric_id:\n metric[\"description\"] = description\n found = True\n break\n\n if not found:\n logger.error(f\"[update_metric_description][Coherence:Failed] Metric {metric_id} not found in dataset {dataset_id}\")\n raise HTTPException(status_code=404, detail=f\"Metric {metric_id} not found in dataset {dataset_id}\")\n\n # PUT back with override_columns=false\n try:\n client.update_dataset(dataset_id, dataset, override_columns=False)\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Superset PUT failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to update dataset in Superset: {e!s}\")\n\n logger.reflect(\n f\"Updated metric {metric_id} description for dataset {dataset_id}\",\n extra={\"src\": \"update_metric_description\"},\n )\n return {\"success\": True, \"description\": description}\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Unexpected error: {e}\")\n raise HTTPException(status_code=500, detail=f\"Unexpected error: {e!s}\")\n# #endregion update_metric_description\n\n# #endregion DatasetsApi\n" + "body": "# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]\n#\n# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [ResourceService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n#\n# @PRE SupersetClient is available; env_id is valid.\n# @POST Returns dataset metadata with mapping status.\n# @SIDE_EFFECT Reads from Superset API and task manager.\n# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse\n# @INVARIANT All dataset responses include last_task metadata\n\n\nimport re\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom pydantic import BaseModel, ConfigDict, Field\n\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_resource_service, get_task_manager, has_permission\n\nrouter = APIRouter(prefix=\"/api/datasets\", tags=[\"Datasets\"])\n\n# #region MappedFields [C:1] [TYPE DataClass]\n# @BRIEF DTO for dataset mapping progress statistics\nclass MappedFields(BaseModel):\n total: int\n mapped: int\n# #endregion MappedFields\n\n# #region LastTask [C:1] [TYPE DataClass]\n# @BRIEF DTO for the most recent task associated with a dataset\nclass LastTask(BaseModel):\n task_id: str | None = None\n status: str | None = Field(None, pattern=\"^RUNNING|SUCCESS|ERROR|WAITING_INPUT$\")\n# #endregion LastTask\n\n# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n metric_count: int = 0\n\n model_config = ConfigDict(validate_by_name=True)\n# #endregion DatasetItem\n\n# #region LinkedDashboard [C:1] [TYPE DataClass]\n# @BRIEF DTO for a dashboard linked to a dataset\nclass LinkedDashboard(BaseModel):\n id: int\n title: str\n slug: str | None = None\n# #endregion LinkedDashboard\n\n# #region DatasetColumn [C:1] [TYPE DataClass]\n# @BRIEF DTO for a single dataset column's metadata\nclass DatasetColumn(BaseModel):\n id: int\n name: str\n type: str | None = None\n is_dttm: bool = False\n is_active: bool = True\n description: str | None = None\n# #endregion DatasetColumn\n\n# #region MetricItem [C:1] [TYPE DataClass]\n# @BRIEF Pydantic DTO for a dataset metric — carries Superset metric metadata.\n# @RELATION DEPENDS_ON -> [DatasetDetailResponse]\nclass MetricItem(BaseModel):\n id: int\n metric_name: str\n expression: str | None = None\n verbose_name: str | None = None\n description: str | None = None\n metric_type: str | None = None\n# #endregion MetricItem\n\n# #region DatasetDetailResponse [C:2] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.\n# @RELATION DEPENDS_ON -> [MetricItem]\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n metrics: list[MetricItem] = []\n metric_count: int = 0\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n model_config = ConfigDict(validate_by_name=True)\n# #endregion DatasetDetailResponse\n\n# #region StatsCounts [C:1] [TYPE DataClass]\n# @BRIEF Aggregate statistics for the Stats Bar — computed from full dataset list.\nclass StatsCounts(BaseModel):\n total: int\n unmapped_count: int\n mapped_count: int\n linked_count: int\n# #endregion StatsCounts\n\n# #region DatasetsResponse [C:2] [TYPE DataClass]\n# @BRIEF Paginated response DTO for dataset listings with stats.\n# @RELATION DEPENDS_ON -> [StatsCounts]\nclass DatasetsResponse(BaseModel):\n datasets: list[DatasetItem]\n stats: StatsCounts\n total: int\n page: int\n page_size: int\n total_pages: int\n# #endregion DatasetsResponse\n\n# #region TaskResponse [C:1] [TYPE DataClass]\n# @BRIEF Response DTO containing a task ID for tracking\nclass TaskResponse(BaseModel):\n task_id: str\n# #endregion TaskResponse\n\n# #region ColumnDescriptionUpdate [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for inline-edit of a column description.\nclass ColumnDescriptionUpdate(BaseModel):\n description: str\n# #endregion ColumnDescriptionUpdate\n\n# #region MetricDescriptionUpdate [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for inline-edit of a metric description.\nclass MetricDescriptionUpdate(BaseModel):\n description: str\n# #endregion MetricDescriptionUpdate\n\n# #region get_dataset_ids [C:4] [TYPE Function]\n# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)\n# @PRE env_id must be a valid environment ID\n# @POST Returns a list of all dataset IDs\n# @RELATION CALLS -> [EXT:method:get_datasets_with_status]\n@router.get(\"/ids\")\nasync def get_dataset_ids(\n env_id: str,\n search: str | None = None,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_ids\", f\"env_id={env_id}, search={search}\"):\n # Validate environment exists\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(f\"[get_dataset_ids][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Extract and return just the IDs\n dataset_ids = [d['id'] for d in datasets]\n logger.info(f\"[get_dataset_ids][Coherence:OK] Returning {len(dataset_ids)} dataset IDs\")\n\n return {\"dataset_ids\": dataset_ids}\n\n except Exception as e:\n logger.error(f\"[get_dataset_ids][Coherence:Failed] Failed to fetch dataset IDs: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset IDs: {e!s}\")\n# #endregion get_dataset_ids\n\n# #region get_datasets [C:4] [TYPE Function]\n# @BRIEF Fetch list of datasets from a specific environment with mapping progress and stats.\n# @PRE env_id must be a valid environment ID\n# @PRE page must be >= 1 if provided\n# @PRE page_size must be between 1 and 100 if provided\n# @POST Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.\n# @POST Response includes pagination metadata (page, page_size, total, total_pages) and stats object.\n# @RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).\n# @RELATION CALLS -> [EXT:method:get_datasets_with_status]\n@router.get(\"\", response_model=DatasetsResponse)\nasync def get_datasets(\n env_id: str,\n search: str | None = None,\n filter: str | None = None,\n page: int = 1,\n page_size: int = 10,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n resource_service=Depends(get_resource_service),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_datasets\", f\"env_id={env_id}, search={search}, filter={filter}, page={page}, page_size={page_size}\"):\n # Validate pagination parameters\n if page < 1:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page: {page}\")\n raise HTTPException(status_code=400, detail=\"Page must be >= 1\")\n if page_size < 1 or page_size > 100:\n logger.error(f\"[get_datasets][Coherence:Failed] Invalid page_size: {page_size}\")\n raise HTTPException(status_code=400, detail=\"Page size must be between 1 and 100\")\n\n # Validate environment exists\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(f\"[get_datasets][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Get all tasks for status lookup\n all_tasks = task_manager.get_all_tasks()\n\n # Fetch datasets with status using ResourceService\n datasets = await resource_service.get_datasets_with_status(env, all_tasks)\n\n # Compute stats from full list BEFORE filtering/pagination\n total_datasets = len(datasets)\n unmapped = sum(\n 1 for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) == 0\n )\n mapped = sum(\n 1 for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) > 0\n )\n linked = sum(\n 1 for d in datasets\n if d.get(\"linked_dashboard_count\", 0) > 0\n )\n stats = StatsCounts(\n total=total_datasets,\n unmapped_count=unmapped,\n mapped_count=mapped,\n linked_count=linked,\n )\n\n # Apply server-side filter (before pagination) — FR-026\n if filter and filter in (\"unmapped\", \"mapped\", \"linked\"):\n if filter == \"unmapped\":\n datasets = [\n d for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) == 0\n ]\n elif filter == \"mapped\":\n datasets = [\n d for d in datasets\n if d.get(\"mapped_fields\") and d[\"mapped_fields\"].get(\"mapped\", 0) > 0\n ]\n elif filter == \"linked\":\n datasets = [\n d for d in datasets\n if d.get(\"linked_dashboard_count\", 0) > 0\n ]\n\n # Apply search filter if provided\n if search:\n search_lower = search.lower()\n datasets = [\n d for d in datasets\n if search_lower in d.get('table_name', '').lower()\n ]\n\n # Calculate pagination\n total = len(datasets)\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n start_idx = (page - 1) * page_size\n end_idx = start_idx + page_size\n\n # Slice datasets for current page\n paginated_datasets = datasets[start_idx:end_idx]\n\n logger.info(f\"[get_datasets][Coherence:OK] Returning {len(paginated_datasets)} datasets (page {page}/{total_pages}, total: {total}, stats: total={stats.total} unmapped={stats.unmapped_count} mapped={stats.mapped_count} linked={stats.linked_count})\")\n\n return DatasetsResponse(\n datasets=paginated_datasets,\n stats=stats,\n total=total,\n page=page,\n page_size=page_size,\n total_pages=total_pages\n )\n\n except Exception as e:\n logger.error(f\"[get_datasets][Coherence:Failed] Failed to fetch datasets: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch datasets: {e!s}\")\n# #endregion get_datasets\n\n# #region MapColumnsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating column mapping\nclass MapColumnsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to map\")\n source_type: str = Field(..., description=\"Source type: 'sqllab' or 'xlsx'\")\n database_id: int | None = Field(None, description=\"Superset database ID for SQL Lab source\")\n sql_query: str | None = Field(None, description=\"Custom SQL query for SQL Lab (optional, defaults to information_schema.columns query)\")\n file_data: str | None = Field(None, description=\"File path or data for XLSX source\")\n# #endregion MapColumnsRequest\n\n# #region map_columns [C:4] [TYPE Function]\n# @BRIEF Trigger bulk column mapping for datasets\n# @PRE User has permission plugin:mapper:execute\n# @PRE env_id is a valid environment ID\n# @PRE dataset_ids is a non-empty list\n# @POST Returns task_id for tracking mapping progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [MapperPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/map-columns\", response_model=TaskResponse)\nasync def map_columns(\n request: MapColumnsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"map_columns\", f\"env={request.env_id}, count={len(request.dataset_ids)}, source={request.source_type}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[map_columns][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate source type\n if request.source_type not in ['sqllab', 'xlsx']:\n logger.error(f\"[map_columns][Coherence:Failed] Invalid source type: {request.source_type}\")\n raise HTTPException(status_code=400, detail=\"Source type must be 'sqllab' or 'xlsx'\")\n\n # Validate database_id for sqllab source\n if request.source_type == 'sqllab' and not request.database_id:\n raise HTTPException(status_code=400, detail=\"database_id is required for 'sqllab' source type\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[map_columns][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create mapping task\n task_params = {\n 'env': request.env_id,\n 'dataset_id': request.dataset_ids[0] if request.dataset_ids else None,\n 'source': 'sqllab' if request.source_type == 'sqllab' else 'excel',\n 'database_id': request.database_id,\n 'sql_query': request.sql_query,\n 'file_data': request.file_data\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='dataset-mapper',\n params=task_params\n )\n\n logger.info(f\"[map_columns][Coherence:OK] Mapping task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[map_columns][Coherence:Failed] Failed to create mapping task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create mapping task: {e!s}\")\n# #endregion map_columns\n\n# #region GenerateDocsRequest [C:1] [TYPE DataClass]\n# @BRIEF Request DTO for initiating documentation generation\nclass GenerateDocsRequest(BaseModel):\n env_id: str = Field(..., description=\"Environment ID\")\n dataset_ids: list[int] = Field(..., description=\"List of dataset IDs to generate docs for\")\n llm_provider: str = Field(..., description=\"LLM provider to use\")\n options: dict | None = Field(None, description=\"Additional options for documentation generation\")\n# #endregion GenerateDocsRequest\n\n# #region generate_docs [C:4] [TYPE Function]\n# @BRIEF Trigger bulk documentation generation for datasets\n# @PRE User has permission plugin:llm_analysis:execute\n# @PRE env_id is a valid environment ID\n# @PRE dataset_ids is a non-empty list\n# @POST Returns task_id for tracking documentation generation progress\n# @POST Task is created and queued for execution\n# @RELATION DISPATCHES -> [DocumentationPlugin]\n# @RELATION CALLS -> [create_task]\n@router.post(\"/generate-docs\", response_model=TaskResponse)\nasync def generate_docs(\n request: GenerateDocsRequest,\n config_manager=Depends(get_config_manager),\n task_manager=Depends(get_task_manager),\n _ = Depends(has_permission(\"plugin:llm_analysis\", \"EXECUTE\"))\n):\n with belief_scope(\"generate_docs\", f\"env={request.env_id}, count={len(request.dataset_ids)}, provider={request.llm_provider}\"):\n # Validate request\n if not request.dataset_ids:\n logger.error(\"[generate_docs][Coherence:Failed] No dataset IDs provided\")\n raise HTTPException(status_code=400, detail=\"At least one dataset ID must be provided\")\n\n # Validate environment exists\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == request.env_id), None)\n\n if not env:\n logger.error(f\"[generate_docs][Coherence:Failed] Environment not found: {request.env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Create documentation generation task\n task_params = {\n 'environment_id': request.env_id,\n 'dataset_id': str(request.dataset_ids[0]) if request.dataset_ids else None,\n 'provider_id': request.llm_provider,\n 'options': request.options or {}\n }\n\n task_obj = await task_manager.create_task(\n plugin_id='llm_documentation',\n params=task_params\n )\n\n logger.info(f\"[generate_docs][Coherence:OK] Documentation generation task created: {task_obj.id} for {len(request.dataset_ids)} datasets\")\n\n return TaskResponse(task_id=str(task_obj.id))\n\n except Exception as e:\n logger.error(f\"[generate_docs][Coherence:Failed] Failed to create documentation generation task: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to create documentation generation task: {e!s}\")\n# #endregion generate_docs\n\n# #region get_dataset_detail [C:4] [TYPE Function]\n# @BRIEF Get detailed dataset information including columns and linked dashboards\n# @PRE env_id is a valid environment ID\n# @PRE dataset_id is a valid dataset ID\n# @POST Returns detailed dataset info with columns and linked dashboards\n# @RELATION CALLS -> [SupersetClientGetDatasetDetail]\n@router.get(\"/{dataset_id}\", response_model=DatasetDetailResponse)\nasync def get_dataset_detail(\n env_id: str,\n dataset_id: int,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"READ\"))\n):\n with belief_scope(\"get_dataset_detail\", f\"env_id={env_id}, dataset_id={dataset_id}\"):\n # Validate environment exists\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(f\"[get_dataset_detail][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n # Fetch detailed dataset info using SupersetClient\n client = SupersetClient(env)\n dataset_detail = client.get_dataset_detail(dataset_id)\n\n # Normalize 'database' field: Superset returns an object, model expects a string\n if isinstance(dataset_detail.get(\"database\"), dict):\n db_obj = dataset_detail[\"database\"]\n dataset_detail[\"database\"] = db_obj.get(\"database_name\", str(db_obj.get(\"id\", \"\")))\n\n logger.info(f\"[get_dataset_detail][Coherence:OK] Retrieved dataset {dataset_id} with {dataset_detail['column_count']} columns and {dataset_detail['linked_dashboard_count']} linked dashboards\")\n\n return DatasetDetailResponse(**dataset_detail)\n\n except Exception as e:\n logger.error(f\"[get_dataset_detail][Coherence:Failed] Failed to fetch dataset detail: {e}\")\n raise HTTPException(status_code=503, detail=f\"Failed to fetch dataset detail: {e!s}\")\n# #endregion get_dataset_detail\n\n# #region _strip_html_tags [C:2] [TYPE Function] [SEMANTICS helper,validation,html]\n# @BRIEF Detect and strip HTML tags from a text string. Uses simple regex <[^>]*>.\n# If HTML is detected, strips tags and logs a warning. Returns cleaned text.\n# @REJECTED Reject-with-400 approach was rejected in favour of silent stripping — more user-friendly\n# and matches the UX expectation that inline-edited descriptions are plain text only.\ndef _strip_html_tags(text: str) -> str:\n if not text:\n return text\n if re.search(r\"<[^>]*>\", text):\n cleaned = re.sub(r\"<[^>]*>\", \"\", text)\n logger.warning(\"[_strip_html_tags] HTML tags detected and stripped from description\")\n return cleaned\n return text\n# #endregion _strip_html_tags\n\n# #region update_column_description [C:4] [TYPE Endpoint]\n# @BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.\n# @PRE dataset_id and column_id must exist in the target environment.\n# @POST Column description in Superset is updated. Response confirms success.\n# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.\n# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or column not found. 502 if Superset upstream fails.\n# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]\n# @RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with override_columns=false.\n# @REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.\n@router.put(\"/{dataset_id}/columns/{column_id}/description\")\nasync def update_column_description(\n dataset_id: int,\n column_id: int,\n env_id: str = Query(..., description=\"Environment ID\"),\n body: ColumnDescriptionUpdate = ...,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"WRITE\"))\n):\n with belief_scope(\"update_column_description\", f\"dataset_id={dataset_id}, column_id={column_id}, env_id={env_id}\"):\n # Validate and sanitize description\n description = _strip_html_tags(body.description)\n if len(description) > 2000:\n logger.error(f\"[update_column_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}\")\n raise HTTPException(status_code=400, detail=\"Description must not exceed 2000 characters\")\n\n # Validate environment\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(f\"[update_column_description][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n\n # GET full dataset\n logger.reason(\"Fetching full dataset from Superset for column description update\",\n extra={\"src\": \"update_column_description\"})\n try:\n response = client.get_dataset(dataset_id)\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Superset GET failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to fetch dataset from Superset: {e!s}\")\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n if not dataset or not dataset.get(\"id\"):\n logger.error(f\"[update_column_description][Coherence:Failed] Dataset {dataset_id} not found\")\n raise HTTPException(status_code=404, detail=f\"Dataset {dataset_id} not found\")\n\n # Find and modify the target column\n columns = dataset.get(\"columns\", [])\n found = False\n for col in columns:\n if col.get(\"id\") == column_id:\n col[\"description\"] = description\n found = True\n break\n\n if not found:\n logger.error(f\"[update_column_description][Coherence:Failed] Column {column_id} not found in dataset {dataset_id}\")\n raise HTTPException(status_code=404, detail=f\"Column {column_id} not found in dataset {dataset_id}\")\n\n # PUT back with override_columns=false\n put_payload = dataset\n try:\n client.update_dataset(dataset_id, put_payload, override_columns=False)\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Superset PUT failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to update dataset in Superset: {e!s}\")\n\n logger.reflect(\n f\"Updated column {column_id} description for dataset {dataset_id}\",\n extra={\"src\": \"update_column_description\"},\n )\n return {\"success\": True, \"description\": description}\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[update_column_description][Coherence:Failed] Unexpected error: {e}\")\n raise HTTPException(status_code=500, detail=f\"Unexpected error: {e!s}\")\n# #endregion update_column_description\n\n# #region update_metric_description [C:4] [TYPE Endpoint]\n# @BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.\n# @PRE dataset_id and metric_id must exist in the target environment.\n# @POST Metric description in Superset is updated.\n# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.\n# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or metric not found. 502 if Superset upstream fails.\n# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]\n@router.put(\"/{dataset_id}/metrics/{metric_id}/description\")\nasync def update_metric_description(\n dataset_id: int,\n metric_id: int,\n env_id: str = Query(..., description=\"Environment ID\"),\n body: MetricDescriptionUpdate = ...,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:migration\", \"WRITE\"))\n):\n with belief_scope(\"update_metric_description\", f\"dataset_id={dataset_id}, metric_id={metric_id}, env_id={env_id}\"):\n # Validate and sanitize description\n description = _strip_html_tags(body.description)\n if len(description) > 2000:\n logger.error(f\"[update_metric_description][Coherence:Failed] Description exceeds 2000 chars: {len(description)}\")\n raise HTTPException(status_code=400, detail=\"Description must not exceed 2000 characters\")\n\n # Validate environment\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(f\"[update_metric_description][Coherence:Failed] Environment not found: {env_id}\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n try:\n client = SupersetClient(env)\n\n # GET full dataset\n logger.reason(\"Fetching full dataset from Superset for metric description update\",\n extra={\"src\": \"update_metric_description\"})\n try:\n response = client.get_dataset(dataset_id)\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Superset GET failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to fetch dataset from Superset: {e!s}\")\n\n if isinstance(response, dict) and \"result\" in response:\n dataset = response[\"result\"]\n else:\n dataset = response\n\n if not dataset or not dataset.get(\"id\"):\n logger.error(f\"[update_metric_description][Coherence:Failed] Dataset {dataset_id} not found\")\n raise HTTPException(status_code=404, detail=f\"Dataset {dataset_id} not found\")\n\n # Find and modify the target metric\n metrics = dataset.get(\"metrics\", [])\n found = False\n for metric in metrics:\n if metric.get(\"id\") == metric_id:\n metric[\"description\"] = description\n found = True\n break\n\n if not found:\n logger.error(f\"[update_metric_description][Coherence:Failed] Metric {metric_id} not found in dataset {dataset_id}\")\n raise HTTPException(status_code=404, detail=f\"Metric {metric_id} not found in dataset {dataset_id}\")\n\n # PUT back with override_columns=false\n try:\n client.update_dataset(dataset_id, dataset, override_columns=False)\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Superset PUT failed: {e}\")\n raise HTTPException(status_code=502, detail=f\"Failed to update dataset in Superset: {e!s}\")\n\n logger.reflect(\n f\"Updated metric {metric_id} description for dataset {dataset_id}\",\n extra={\"src\": \"update_metric_description\"},\n )\n return {\"success\": True, \"description\": description}\n\n except HTTPException:\n raise\n except Exception as e:\n logger.error(f\"[update_metric_description][Coherence:Failed] Unexpected error: {e}\")\n raise HTTPException(status_code=500, detail=f\"Unexpected error: {e!s}\")\n# #endregion update_metric_description\n\n# #endregion DatasetsApi\n" }, { "contract_id": "MappedFields", @@ -9806,7 +10655,7 @@ "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", "start_line": 41, - "end_line": 54, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9817,14 +10666,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n metric_count: int = 0\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetItem\n" + "body": "# #region DatasetItem [C:1] [TYPE DataClass]\n# @BRIEF Summary DTO for a dataset in the hub listing\nclass DatasetItem(BaseModel):\n id: int\n table_name: str\n schema_name: str = Field(..., alias=\"schema\")\n database: str\n mapped_fields: MappedFields | None = None\n last_task: LastTask | None = None\n metric_count: int = 0\n\n model_config = ConfigDict(validate_by_name=True)\n# #endregion DatasetItem\n" }, { "contract_id": "LinkedDashboard", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 56, - "end_line": 62, + "start_line": 55, + "end_line": 61, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9841,8 +10690,8 @@ "contract_id": "DatasetColumn", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 64, - "end_line": 73, + "start_line": 63, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9859,8 +10708,8 @@ "contract_id": "MetricItem", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 75, - "end_line": 85, + "start_line": 74, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9884,8 +10733,8 @@ "contract_id": "DatasetDetailResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 87, - "end_line": 109, + "start_line": 86, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -9903,14 +10752,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetDetailResponse [C:2] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.\n# @RELATION DEPENDS_ON -> [MetricItem]\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n metrics: list[MetricItem] = []\n metric_count: int = 0\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n class Config:\n allow_population_by_field_name = True\n# #endregion DatasetDetailResponse\n" + "body": "# #region DatasetDetailResponse [C:2] [TYPE DataClass]\n# @BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.\n# @RELATION DEPENDS_ON -> [MetricItem]\nclass DatasetDetailResponse(BaseModel):\n id: int\n table_name: str | None = None\n schema_name: str | None = Field(None, alias=\"schema\")\n database: str\n description: str | None = None\n columns: list[DatasetColumn]\n column_count: int\n metrics: list[MetricItem] = []\n metric_count: int = 0\n sql: str | None = None\n linked_dashboards: list[LinkedDashboard]\n linked_dashboard_count: int\n is_sqllab_view: bool = False\n created_on: str | None = None\n changed_on: str | None = None\n\n model_config = ConfigDict(validate_by_name=True)\n# #endregion DatasetDetailResponse\n" }, { "contract_id": "StatsCounts", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 111, - "end_line": 118, + "start_line": 109, + "end_line": 116, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9927,8 +10776,8 @@ "contract_id": "DatasetsResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 120, - "end_line": 130, + "start_line": 118, + "end_line": 128, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -9952,8 +10801,8 @@ "contract_id": "TaskResponse", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 132, - "end_line": 136, + "start_line": 130, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9970,8 +10819,8 @@ "contract_id": "ColumnDescriptionUpdate", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 138, - "end_line": 142, + "start_line": 136, + "end_line": 140, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -9988,8 +10837,8 @@ "contract_id": "MetricDescriptionUpdate", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 144, - "end_line": 148, + "start_line": 142, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -10006,8 +10855,8 @@ "contract_id": "get_dataset_ids", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 150, - "end_line": 196, + "start_line": 148, + "end_line": 194, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10033,8 +10882,8 @@ "contract_id": "get_datasets", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 198, - "end_line": 312, + "start_line": 196, + "end_line": 310, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10061,8 +10910,8 @@ "contract_id": "MapColumnsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 314, - "end_line": 323, + "start_line": 312, + "end_line": 321, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -10079,8 +10928,8 @@ "contract_id": "map_columns", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 325, - "end_line": 387, + "start_line": 323, + "end_line": 385, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10112,8 +10961,8 @@ "contract_id": "GenerateDocsRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 389, - "end_line": 396, + "start_line": 387, + "end_line": 394, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -10130,8 +10979,8 @@ "contract_id": "generate_docs", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 398, - "end_line": 449, + "start_line": 396, + "end_line": 447, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10163,8 +11012,8 @@ "contract_id": "get_dataset_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 451, - "end_line": 489, + "start_line": 449, + "end_line": 487, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10190,8 +11039,8 @@ "contract_id": "_strip_html_tags", "contract_type": "Function", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 491, - "end_line": 504, + "start_line": 489, + "end_line": 502, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -10202,14 +11051,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _strip_html_tags [C:2] [TYPE Function] [SEMANTICS helper,validation,html]\n# @BRIEF Detect and strip HTML tags from a text string. Uses simple regex <[^>]*>.\n# If HTML is detected, strips tags and logs a warning. Returns cleaned text.\n# @REJECTED Reject-with-400 approach was rejected in favour of silent stripping — more user-friendly\n# and matches the UX expectation that inline-edited descriptions are plain text only.\ndef _strip_html_tags(text: str) -> str:\n if not text:\n return text\n if re.search(r\"<[^>]*>\", text):\n cleaned = re.sub(r\"<[^>]*>\", \"\", text)\n logger.warning(f\"[_strip_html_tags] HTML tags detected and stripped from description\")\n return cleaned\n return text\n# #endregion _strip_html_tags\n" + "body": "# #region _strip_html_tags [C:2] [TYPE Function] [SEMANTICS helper,validation,html]\n# @BRIEF Detect and strip HTML tags from a text string. Uses simple regex <[^>]*>.\n# If HTML is detected, strips tags and logs a warning. Returns cleaned text.\n# @REJECTED Reject-with-400 approach was rejected in favour of silent stripping — more user-friendly\n# and matches the UX expectation that inline-edited descriptions are plain text only.\ndef _strip_html_tags(text: str) -> str:\n if not text:\n return text\n if re.search(r\"<[^>]*>\", text):\n cleaned = re.sub(r\"<[^>]*>\", \"\", text)\n logger.warning(\"[_strip_html_tags] HTML tags detected and stripped from description\")\n return cleaned\n return text\n# #endregion _strip_html_tags\n" }, { "contract_id": "update_column_description", "contract_type": "Endpoint", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 506, - "end_line": 593, + "start_line": 504, + "end_line": 591, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10246,8 +11095,8 @@ "contract_id": "update_metric_description", "contract_type": "Endpoint", "file_path": "backend/src/api/routes/datasets.py", - "start_line": 595, - "end_line": 679, + "start_line": 593, + "end_line": 677, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -10889,7 +11738,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]\n# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.api.routes.git_schemas import (\n MergeConflictFileSchema,\n MergeContinueRequest,\n MergeResolveRequest,\n MergeStatusSchema,\n)\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom ._deps import get_git_service\nfrom ._helpers import (\n _handle_unexpected_git_route_error,\n)\nfrom ._router import router\n\n\n# #region get_merge_status [C:2] [TYPE Function]\n# @BRIEF Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# #endregion get_merge_status\n\n\n# #region get_merge_conflicts [C:2] [TYPE Function]\n# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=list[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# #endregion get_merge_conflicts\n\n\n# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n\n\n# #region abort_merge [C:2] [TYPE Function]\n# @BRIEF Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# #endregion abort_merge\n\n\n# #region continue_merge [C:3] [TYPE Function]\n# @BRIEF Finalize unfinished merge from WebUI flow.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# #endregion continue_merge\n# #endregion GitMergeRoutes\n" + "body": "# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]\n# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException\n\nfrom src.api.routes.git_schemas import (\n MergeConflictFileSchema,\n MergeContinueRequest,\n MergeResolveRequest,\n MergeStatusSchema,\n)\nfrom src.core.logger import belief_scope\nfrom src.dependencies import get_config_manager, has_permission\n\nfrom ._deps import get_git_service\nfrom ._helpers import (\n _handle_unexpected_git_route_error,\n)\nfrom ._router import router\n\n\n# #region get_merge_status [C:2] [TYPE Function]\n# @BRIEF Return unfinished-merge status for repository (web-only recovery support).\n@router.get(\n \"/repositories/{dashboard_ref}/merge/status\", response_model=MergeStatusSchema\n)\nasync def get_merge_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_status\", e)\n# #endregion get_merge_status\n\n\n# #region get_merge_conflicts [C:2] [TYPE Function]\n# @BRIEF Return conflicted files with mine/theirs previews for web conflict resolver.\n@router.get(\n \"/repositories/{dashboard_ref}/merge/conflicts\",\n response_model=list[MergeConflictFileSchema],\n)\nasync def get_merge_conflicts(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_merge_conflicts(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_merge_conflicts\", e)\n# #endregion get_merge_conflicts\n\n\n# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.model_dump() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n\n\n# #region abort_merge [C:2] [TYPE Function]\n# @BRIEF Abort unfinished merge from WebUI flow.\n@router.post(\"/repositories/{dashboard_ref}/merge/abort\")\nasync def abort_merge(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"abort_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.abort_merge(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"abort_merge\", e)\n# #endregion abort_merge\n\n\n# #region continue_merge [C:3] [TYPE Function]\n# @BRIEF Finalize unfinished merge from WebUI flow.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/continue\")\nasync def continue_merge(\n dashboard_ref: str,\n continue_data: MergeContinueRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"continue_merge\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.continue_merge(dashboard_id, continue_data.message)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"continue_merge\", e)\n# #endregion continue_merge\n# #endregion GitMergeRoutes\n" }, { "contract_id": "get_merge_status", @@ -10950,7 +11799,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.dict() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n" + "body": "# #region resolve_merge_conflicts [C:3] [TYPE Function]\n# @BRIEF Apply mine/theirs/manual conflict resolutions from WebUI and stage files.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/merge/resolve\")\nasync def resolve_merge_conflicts(\n dashboard_ref: str,\n resolve_data: MergeResolveRequest,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"resolve_merge_conflicts\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n resolved_files = _gs.resolve_merge_conflicts(\n dashboard_id,\n [item.model_dump() for item in resolve_data.resolutions],\n )\n return {\"status\": \"success\", \"resolved_files\": resolved_files}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"resolve_merge_conflicts\", e)\n# #endregion resolve_merge_conflicts\n" }, { "contract_id": "abort_merge", @@ -11106,7 +11955,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]\n# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n CommitCreate,\n CommitSchema,\n RepoStatusBatchRequest,\n RepoStatusBatchResponse,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.git import GitRepository, GitServerConfig\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _build_no_repo_status_payload,\n _handle_unexpected_git_route_error,\n _resolve_repository_status,\n)\nfrom ._router import router\n\n\n# #region commit_changes [C:2] [TYPE Function]\n# @BRIEF Stage and commit changes in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/commit\")\nasync def commit_changes(\n dashboard_ref: str,\n commit_data: CommitCreate,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"commit_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.commit_changes(\n dashboard_id, commit_data.message, commit_data.files\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"commit_changes\", e)\n# #endregion commit_changes\n\n\n# #region push_changes [C:2] [TYPE Function]\n# @BRIEF Push local commits to the remote repository.\n@router.post(\"/repositories/{dashboard_ref}/push\")\nasync def push_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"push_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _gs.push_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"push_changes\", e)\n# #endregion push_changes\n\n\n# #region pull_changes [C:3] [TYPE Function]\n# @BRIEF Pull changes from the remote repository.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/pull\")\nasync def pull_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"pull_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = None\n config_url = None\n config_provider = None\n try:\n db_repo_candidate = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if getattr(db_repo_candidate, \"config_id\", None):\n db_repo = db_repo_candidate\n config_row = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if config_row:\n config_url = config_row.url\n config_provider = config_row.provider\n except Exception as diagnostics_error:\n logger.explore(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"pull_changes\", \"payload\": {\"dashboard_id\": dashboard_id}, \"error\": str(diagnostics_error)},\n )\n logger.reason(\n f\"Route diagnostics dashboard_ref={dashboard_ref} env_id={env_id} resolved_dashboard_id={dashboard_id} \"\n f\"binding_exists={bool(db_repo)} binding_local_path={(db_repo.local_path if db_repo else None)} \"\n f\"binding_remote_url={(db_repo.remote_url if db_repo else None)} \"\n f\"binding_config_id={(db_repo.config_id if db_repo else None)} \"\n f\"config_provider={config_provider} config_url={config_url}\",\n extra={\"src\": \"pull_changes\"},\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.pull_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"pull_changes\", e)\n# #endregion pull_changes\n\n\n# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref_async\n\n try:\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n\n\n# #region get_repository_status_batch [C:2] [TYPE Function]\n# @BRIEF Get Git statuses for multiple dashboard repositories in one request.\n@router.post(\"/repositories/status/batch\", response_model=RepoStatusBatchResponse)\nasync def get_repository_status_batch(\n request: RepoStatusBatchRequest,\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status_batch\"):\n dashboard_ids = list(dict.fromkeys(request.dashboard_ids))\n if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH:\n logger.reason(\n f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\",\n extra={\"src\": \"get_repository_status_batch\"},\n )\n dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH]\n\n statuses = {}\n for dashboard_id in dashboard_ids:\n try:\n statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)\n except HTTPException:\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n except Exception as e:\n logger.error(\n f\"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}\"\n )\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n return RepoStatusBatchResponse(statuses=statuses)\n# #endregion get_repository_status_batch\n\n\n# #region get_repository_diff [C:2] [TYPE Function]\n# @BRIEF Get Git diff for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/diff\")\nasync def get_repository_diff(\n dashboard_ref: str,\n file_path: str | None = None,\n staged: bool = False,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_repository_diff\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_diff(dashboard_id, file_path, staged)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_diff\", e)\n# #endregion get_repository_diff\n\n\n# #region get_history [C:2] [TYPE Function]\n# @BRIEF View commit history for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/history\", response_model=list[CommitSchema])\nasync def get_history(\n dashboard_ref: str,\n limit: int = 50,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_history\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_commit_history(dashboard_id, limit)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_history\", e)\n# #endregion get_history\n\n\n# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from src.plugins.llm_analysis.models import LLMProviderType\n from src.plugins.llm_analysis.service import LLMClient\n from src.services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from src.services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from src.plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n# #endregion GitRepoOperationsRoutes\n" + "body": "# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]\n# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom src.api.routes.git_schemas import (\n CommitCreate,\n CommitSchema,\n RepoStatusBatchRequest,\n RepoStatusBatchResponse,\n)\nfrom src.core.database import get_db\nfrom src.core.logger import belief_scope, logger\nfrom src.dependencies import get_config_manager, get_current_user, has_permission\nfrom src.models.auth import User\nfrom src.models.git import GitRepository, GitServerConfig\n\nfrom ._deps import MAX_REPOSITORY_STATUS_BATCH, get_git_service\nfrom ._helpers import (\n _apply_git_identity_from_profile,\n _build_no_repo_status_payload,\n _handle_unexpected_git_route_error,\n _resolve_repository_status,\n)\nfrom ._router import router\n\n\n# #region commit_changes [C:2] [TYPE Function]\n# @BRIEF Stage and commit changes in the dashboard's repository.\n@router.post(\"/repositories/{dashboard_ref}/commit\")\nasync def commit_changes(\n dashboard_ref: str,\n commit_data: CommitCreate,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"commit_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.commit_changes(\n dashboard_id, commit_data.message, commit_data.files\n )\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"commit_changes\", e)\n# #endregion commit_changes\n\n\n# #region push_changes [C:2] [TYPE Function]\n# @BRIEF Push local commits to the remote repository.\n@router.post(\"/repositories/{dashboard_ref}/push\")\nasync def push_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"push_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n _gs.push_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"push_changes\", e)\n# #endregion push_changes\n\n\n# #region pull_changes [C:3] [TYPE Function]\n# @BRIEF Pull changes from the remote repository.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/pull\")\nasync def pull_changes(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"pull_changes\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n db_repo = None\n config_url = None\n config_provider = None\n try:\n db_repo_candidate = (\n db.query(GitRepository)\n .filter(GitRepository.dashboard_id == dashboard_id)\n .first()\n )\n if getattr(db_repo_candidate, \"config_id\", None):\n db_repo = db_repo_candidate\n config_row = (\n db.query(GitServerConfig)\n .filter(GitServerConfig.id == db_repo.config_id)\n .first()\n )\n if config_row:\n config_url = config_row.url\n config_provider = config_row.provider\n except Exception as diagnostics_error:\n logger.explore(\n \"Failed to load repository binding diagnostics\",\n extra={\"src\": \"pull_changes\", \"payload\": {\"dashboard_id\": dashboard_id}, \"error\": str(diagnostics_error)},\n )\n logger.reason(\n f\"Route diagnostics dashboard_ref={dashboard_ref} env_id={env_id} resolved_dashboard_id={dashboard_id} \"\n f\"binding_exists={bool(db_repo)} binding_local_path={(db_repo.local_path if db_repo else None)} \"\n f\"binding_remote_url={(db_repo.remote_url if db_repo else None)} \"\n f\"binding_config_id={(db_repo.config_id if db_repo else None)} \"\n f\"config_provider={config_provider} config_url={config_url}\",\n extra={\"src\": \"pull_changes\"},\n )\n _apply_git_identity_from_profile(dashboard_id, db, current_user)\n _gs.pull_changes(dashboard_id)\n return {\"status\": \"success\"}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"pull_changes\", e)\n# #endregion pull_changes\n\n\n# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n\n\n# #region get_repository_status_batch [C:2] [TYPE Function]\n# @BRIEF Get Git statuses for multiple dashboard repositories in one request.\n@router.post(\"/repositories/status/batch\", response_model=RepoStatusBatchResponse)\nasync def get_repository_status_batch(\n request: RepoStatusBatchRequest,\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status_batch\"):\n dashboard_ids = list(dict.fromkeys(request.dashboard_ids))\n if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH:\n logger.reason(\n f\"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.\",\n extra={\"src\": \"get_repository_status_batch\"},\n )\n dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH]\n\n statuses = {}\n for dashboard_id in dashboard_ids:\n try:\n statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)\n except HTTPException:\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n except Exception as e:\n logger.error(\n f\"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}\"\n )\n statuses[str(dashboard_id)] = {\n **_build_no_repo_status_payload(),\n \"sync_state\": \"ERROR\",\n \"sync_status\": \"ERROR\",\n }\n return RepoStatusBatchResponse(statuses=statuses)\n# #endregion get_repository_status_batch\n\n\n# #region get_repository_diff [C:2] [TYPE Function]\n# @BRIEF Get Git diff for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/diff\")\nasync def get_repository_diff(\n dashboard_ref: str,\n file_path: str | None = None,\n staged: bool = False,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_repository_diff\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_diff(dashboard_id, file_path, staged)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_diff\", e)\n# #endregion get_repository_diff\n\n\n# #region get_history [C:2] [TYPE Function]\n# @BRIEF View commit history for a dashboard's repository.\n@router.get(\"/repositories/{dashboard_ref}/history\", response_model=list[CommitSchema])\nasync def get_history(\n dashboard_ref: str,\n limit: int = 50,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"get_history\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _gs.get_commit_history(dashboard_id, limit)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_history\", e)\n# #endregion get_history\n\n\n# #region generate_commit_message [C:3] [TYPE Function]\n# @BRIEF Generate a suggested commit message using LLM.\n# @RELATION CALLS -> [GitService]\n@router.post(\"/repositories/{dashboard_ref}/generate-message\")\nasync def generate_commit_message(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n _gs = get_git_service()\n with belief_scope(\"generate_commit_message\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n diff = _gs.get_diff(dashboard_id, staged=True)\n if not diff:\n diff = _gs.get_diff(dashboard_id, staged=False)\n\n if not diff:\n return {\"message\": \"No changes detected\"}\n\n history_objs = _gs.get_commit_history(dashboard_id, limit=5)\n history = [h.message for h in history_objs if hasattr(h, \"message\")]\n\n from src.plugins.llm_analysis.models import LLMProviderType\n from src.plugins.llm_analysis.service import LLMClient\n from src.services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n resolve_bound_provider_id,\n )\n from src.services.llm_provider import LLMProviderService\n\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_settings = normalize_llm_settings(\n config_manager.get_config().settings.llm\n )\n bound_provider_id = resolve_bound_provider_id(llm_settings, \"git_commit\")\n provider = next((p for p in providers if p.id == bound_provider_id), None)\n if not provider:\n provider = next((p for p in providers if p.is_active), None)\n\n if not provider:\n raise HTTPException(\n status_code=400, detail=\"No active LLM provider found\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider.id)\n client = LLMClient(\n provider_type=LLMProviderType(provider.provider_type),\n api_key=api_key,\n base_url=provider.base_url,\n default_model=provider.default_model,\n )\n\n from src.plugins.git.llm_extension import GitLLMExtension\n\n extension = GitLLMExtension(client)\n git_prompt = llm_settings[\"prompts\"].get(\n \"git_commit_prompt\",\n DEFAULT_LLM_PROMPTS[\"git_commit_prompt\"],\n )\n message = await extension.suggest_commit_message(\n diff,\n history,\n prompt_template=git_prompt,\n )\n return {\"message\": message}\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"generate_commit_message\", e)\n# #endregion generate_commit_message\n# #endregion GitRepoOperationsRoutes\n" }, { "contract_id": "commit_changes", @@ -11185,7 +12034,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref_async\n\n try:\n dashboard_id = await _resolve_dashboard_id_from_ref_async(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n" + "body": "# #region get_repository_status [C:2] [TYPE Function]\n# @BRIEF Get current Git status for a dashboard repository.\n@router.get(\"/repositories/{dashboard_ref}/status\")\nasync def get_repository_status(\n dashboard_ref: str,\n env_id: str | None = None,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:git\", \"EXECUTE\")),\n):\n with belief_scope(\"get_repository_status\"):\n from . import _resolve_dashboard_id_from_ref\n\n try:\n dashboard_id = _resolve_dashboard_id_from_ref(\n dashboard_ref, config_manager, env_id\n )\n return _resolve_repository_status(dashboard_id)\n except HTTPException:\n raise\n except Exception as e:\n _handle_unexpected_git_route_error(\"get_repository_status\", e)\n# #endregion get_repository_status\n" }, { "contract_id": "get_repository_status_batch", @@ -11406,7 +12255,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/git_schemas.py", "start_line": 1, - "end_line": 293, + "end_line": 290, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11426,7 +12275,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS fastapi, git, api, git-server-config-base]\n#\n# @BRIEF Defines Pydantic models for the Git integration API layer.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [GitModels]\n#\n# @INVARIANT All schemas must be compatible with the FastAPI router.\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom src.models.git import GitProvider, GitStatus, SyncStatus\n\n\n# #region GitServerConfigBase [C:1] [TYPE Class]\n# @BRIEF 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: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(\"main\", description=\"Default branch logic/name\")\n# #endregion GitServerConfigBase\n\n# #region GitServerConfigUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: str | None = Field(None, description=\"Display name for the Git server\")\n provider: GitProvider | None = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str | None = Field(None, description=\"Server base URL\")\n pat: str | None = Field(None, description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(None, description=\"Default branch logic/name\")\n# #endregion GitServerConfigUpdate\n\n# #region GitServerConfigCreate [TYPE Class]\n# @BRIEF Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: str | None = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# #endregion GitServerConfigCreate\n\n# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF 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# #endregion GitServerConfigSchema\n\n# #region GitRepositorySchema [TYPE Class]\n# @BRIEF 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# #endregion GitRepositorySchema\n\n# #region BranchSchema [TYPE Class]\n# @BRIEF 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# #endregion BranchSchema\n\n# #region CommitSchema [TYPE Class]\n# @BRIEF 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# #endregion CommitSchema\n\n# #region BranchCreate [TYPE Class]\n# @BRIEF Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# #endregion BranchCreate\n\n# #region BranchCheckout [TYPE Class]\n# @BRIEF Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# #endregion BranchCheckout\n\n# #region CommitCreate [TYPE Class]\n# @BRIEF Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: list[str]\n# #endregion CommitCreate\n\n# #region ConflictResolution [TYPE Class]\n# @BRIEF 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: str | None = None\n# #endregion ConflictResolution\n\n\n# #region MergeStatusSchema [TYPE Class]\n# @BRIEF 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: str | None = None\n merge_message_preview: str | None = None\n conflicts_count: int = 0\n# #endregion MergeStatusSchema\n\n\n# #region MergeConflictFileSchema [TYPE Class]\n# @BRIEF Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: str | None = None\n theirs: str | None = None\n# #endregion MergeConflictFileSchema\n\n\n# #region MergeResolveRequest [TYPE Class]\n# @BRIEF Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: list[ConflictResolution] = Field(default_factory=list)\n# #endregion MergeResolveRequest\n\n\n# #region MergeContinueRequest [TYPE Class]\n# @BRIEF Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: str | None = None\n# #endregion MergeContinueRequest\n\n# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF 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# #endregion DeploymentEnvironmentSchema\n\n# #region DeployRequest [TYPE Class]\n# @BRIEF Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# #endregion DeployRequest\n\n# #region RepoInitRequest [TYPE Class]\n# @BRIEF Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# #endregion RepoInitRequest\n\n\n# #region RepositoryBindingSchema [TYPE Class]\n# @BRIEF 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# #endregion RepositoryBindingSchema\n\n# #region RepoStatusBatchRequest [TYPE Class]\n# @BRIEF 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# #endregion RepoStatusBatchRequest\n\n\n# #region RepoStatusBatchResponse [TYPE Class]\n# @BRIEF Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: dict[str, dict[str, Any]]\n# #endregion RepoStatusBatchResponse\n\n\n# #region GiteaRepoSchema [TYPE Class]\n# @BRIEF Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion GiteaRepoSchema\n\n\n# #region GiteaRepoCreateRequest [TYPE Class]\n# @BRIEF 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: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion GiteaRepoCreateRequest\n\n\n# #region RemoteRepoSchema [TYPE Class]\n# @BRIEF 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: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion RemoteRepoSchema\n\n\n# #region RemoteRepoCreateRequest [TYPE Class]\n# @BRIEF Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion RemoteRepoCreateRequest\n\n\n# #region PromoteRequest [TYPE Class]\n# @BRIEF 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: str | None = None\n description: str | None = None\n reason: str | None = None\n draft: bool = False\n remove_source_branch: bool = False\n# #endregion PromoteRequest\n\n\n# #region PromoteResponse [TYPE Class]\n# @BRIEF 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: str | None = None\n reference_id: str | None = None\n policy_violation: bool = False\n# #endregion PromoteResponse\n\n# #endregion GitSchemas\n" + "body": "# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS fastapi, git, api, git-server-config-base]\n#\n# @BRIEF Defines Pydantic models for the Git integration API layer.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [GitModels]\n#\n# @INVARIANT All schemas must be compatible with the FastAPI router.\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\nfrom src.models.git import GitProvider, GitStatus, SyncStatus\n\n\n# #region GitServerConfigBase [C:1] [TYPE Class]\n# @BRIEF 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: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(\"main\", description=\"Default branch logic/name\")\n# #endregion GitServerConfigBase\n\n# #region GitServerConfigUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing Git server configuration.\nclass GitServerConfigUpdate(BaseModel):\n name: str | None = Field(None, description=\"Display name for the Git server\")\n provider: GitProvider | None = Field(None, description=\"Git provider (GITHUB, GITLAB, GITEA)\")\n url: str | None = Field(None, description=\"Server base URL\")\n pat: str | None = Field(None, description=\"Personal Access Token\")\n default_repository: str | None = Field(None, description=\"Default repository path (org/repo)\")\n default_branch: str | None = Field(None, description=\"Default branch logic/name\")\n# #endregion GitServerConfigUpdate\n\n# #region GitServerConfigCreate [TYPE Class]\n# @BRIEF Schema for creating a new Git server configuration.\nclass GitServerConfigCreate(GitServerConfigBase):\n \"\"\"Schema for creating a new Git server configuration.\"\"\"\n config_id: str | None = Field(None, description=\"Optional config ID, useful for testing an existing config without sending its full PAT\")\n# #endregion GitServerConfigCreate\n\n# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion GitServerConfigSchema\n\n# #region GitRepositorySchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion GitRepositorySchema\n\n# #region BranchSchema [TYPE Class]\n# @BRIEF 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# #endregion BranchSchema\n\n# #region CommitSchema [TYPE Class]\n# @BRIEF 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# #endregion CommitSchema\n\n# #region BranchCreate [TYPE Class]\n# @BRIEF Schema for branch creation requests.\nclass BranchCreate(BaseModel):\n \"\"\"Schema for branch creation requests.\"\"\"\n name: str\n from_branch: str\n# #endregion BranchCreate\n\n# #region BranchCheckout [TYPE Class]\n# @BRIEF Schema for branch checkout requests.\nclass BranchCheckout(BaseModel):\n \"\"\"Schema for branch checkout requests.\"\"\"\n name: str\n# #endregion BranchCheckout\n\n# #region CommitCreate [TYPE Class]\n# @BRIEF Schema for staging and committing changes.\nclass CommitCreate(BaseModel):\n \"\"\"Schema for staging and committing changes.\"\"\"\n message: str\n files: list[str]\n# #endregion CommitCreate\n\n# #region ConflictResolution [TYPE Class]\n# @BRIEF 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: str | None = None\n# #endregion ConflictResolution\n\n\n# #region MergeStatusSchema [TYPE Class]\n# @BRIEF 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: str | None = None\n merge_message_preview: str | None = None\n conflicts_count: int = 0\n# #endregion MergeStatusSchema\n\n\n# #region MergeConflictFileSchema [TYPE Class]\n# @BRIEF Schema describing one conflicted file with optional side snapshots.\nclass MergeConflictFileSchema(BaseModel):\n file_path: str\n mine: str | None = None\n theirs: str | None = None\n# #endregion MergeConflictFileSchema\n\n\n# #region MergeResolveRequest [TYPE Class]\n# @BRIEF Request schema for resolving one or multiple merge conflicts.\nclass MergeResolveRequest(BaseModel):\n resolutions: list[ConflictResolution] = Field(default_factory=list)\n# #endregion MergeResolveRequest\n\n\n# #region MergeContinueRequest [TYPE Class]\n# @BRIEF Request schema for finishing merge with optional explicit commit message.\nclass MergeContinueRequest(BaseModel):\n message: str | None = None\n# #endregion MergeContinueRequest\n\n# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion DeploymentEnvironmentSchema\n\n# #region DeployRequest [TYPE Class]\n# @BRIEF Schema for dashboard deployment requests.\nclass DeployRequest(BaseModel):\n \"\"\"Schema for deployment requests.\"\"\"\n environment_id: str\n# #endregion DeployRequest\n\n# #region RepoInitRequest [TYPE Class]\n# @BRIEF Schema for repository initialization requests.\nclass RepoInitRequest(BaseModel):\n \"\"\"Schema for repository initialization requests.\"\"\"\n config_id: str\n remote_url: str\n# #endregion RepoInitRequest\n\n\n# #region RepositoryBindingSchema [TYPE Class]\n# @BRIEF 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# #endregion RepositoryBindingSchema\n\n# #region RepoStatusBatchRequest [TYPE Class]\n# @BRIEF 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# #endregion RepoStatusBatchRequest\n\n\n# #region RepoStatusBatchResponse [TYPE Class]\n# @BRIEF Schema for returning repository statuses keyed by dashboard ID.\nclass RepoStatusBatchResponse(BaseModel):\n statuses: dict[str, dict[str, Any]]\n# #endregion RepoStatusBatchResponse\n\n\n# #region GiteaRepoSchema [TYPE Class]\n# @BRIEF Schema describing a Gitea repository.\nclass GiteaRepoSchema(BaseModel):\n name: str\n full_name: str\n private: bool = False\n clone_url: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion GiteaRepoSchema\n\n\n# #region GiteaRepoCreateRequest [TYPE Class]\n# @BRIEF 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: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion GiteaRepoCreateRequest\n\n\n# #region RemoteRepoSchema [TYPE Class]\n# @BRIEF 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: str | None = None\n html_url: str | None = None\n ssh_url: str | None = None\n default_branch: str | None = None\n# #endregion RemoteRepoSchema\n\n\n# #region RemoteRepoCreateRequest [TYPE Class]\n# @BRIEF Provider-agnostic repository creation request.\nclass RemoteRepoCreateRequest(BaseModel):\n name: str = Field(..., min_length=1, max_length=255)\n private: bool = True\n description: str | None = None\n auto_init: bool = True\n default_branch: str | None = \"main\"\n# #endregion RemoteRepoCreateRequest\n\n\n# #region PromoteRequest [TYPE Class]\n# @BRIEF 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: str | None = None\n description: str | None = None\n reason: str | None = None\n draft: bool = False\n remove_source_branch: bool = False\n# #endregion PromoteRequest\n\n\n# #region PromoteResponse [TYPE Class]\n# @BRIEF 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: str | None = None\n reference_id: str | None = None\n policy_violation: bool = False\n# #endregion PromoteResponse\n\n# #endregion GitSchemas\n" }, { "contract_id": "GitServerConfigBase", @@ -11485,7 +12334,7 @@ "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", "start_line": 47, - "end_line": 57, + "end_line": 56, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11495,14 +12344,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF 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# #endregion GitServerConfigSchema\n" + "body": "# #region GitServerConfigSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion GitServerConfigSchema\n" }, { "contract_id": "GitRepositorySchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 59, - "end_line": 73, + "start_line": 58, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11512,14 +12361,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitRepositorySchema [TYPE Class]\n# @BRIEF 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# #endregion GitRepositorySchema\n" + "body": "# #region GitRepositorySchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion GitRepositorySchema\n" }, { "contract_id": "BranchSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 75, - "end_line": 83, + "start_line": 73, + "end_line": 81, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11535,8 +12384,8 @@ "contract_id": "CommitSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 85, - "end_line": 95, + "start_line": 83, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11552,8 +12401,8 @@ "contract_id": "BranchCreate", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 97, - "end_line": 103, + "start_line": 95, + "end_line": 101, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11569,8 +12418,8 @@ "contract_id": "BranchCheckout", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 105, - "end_line": 110, + "start_line": 103, + "end_line": 108, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11586,8 +12435,8 @@ "contract_id": "CommitCreate", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 112, - "end_line": 118, + "start_line": 110, + "end_line": 116, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11603,8 +12452,8 @@ "contract_id": "ConflictResolution", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 120, - "end_line": 127, + "start_line": 118, + "end_line": 125, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11620,8 +12469,8 @@ "contract_id": "MergeStatusSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 130, - "end_line": 140, + "start_line": 128, + "end_line": 138, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11637,8 +12486,8 @@ "contract_id": "MergeConflictFileSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 143, - "end_line": 149, + "start_line": 141, + "end_line": 147, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11654,8 +12503,8 @@ "contract_id": "MergeResolveRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 152, - "end_line": 156, + "start_line": 150, + "end_line": 154, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11671,8 +12520,8 @@ "contract_id": "MergeContinueRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 159, - "end_line": 163, + "start_line": 157, + "end_line": 161, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11688,8 +12537,8 @@ "contract_id": "DeploymentEnvironmentSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 165, - "end_line": 176, + "start_line": 163, + "end_line": 173, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11699,14 +12548,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF 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# #endregion DeploymentEnvironmentSchema\n" + "body": "# #region DeploymentEnvironmentSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n# #endregion DeploymentEnvironmentSchema\n" }, { "contract_id": "DeployRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 178, - "end_line": 183, + "start_line": 175, + "end_line": 180, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11722,8 +12571,8 @@ "contract_id": "RepoInitRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 185, - "end_line": 191, + "start_line": 182, + "end_line": 188, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11739,8 +12588,8 @@ "contract_id": "RepositoryBindingSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 194, - "end_line": 202, + "start_line": 191, + "end_line": 199, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11756,8 +12605,8 @@ "contract_id": "RepoStatusBatchRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 204, - "end_line": 208, + "start_line": 201, + "end_line": 205, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11773,8 +12622,8 @@ "contract_id": "RepoStatusBatchResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 211, - "end_line": 215, + "start_line": 208, + "end_line": 212, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11790,8 +12639,8 @@ "contract_id": "GiteaRepoSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 218, - "end_line": 228, + "start_line": 215, + "end_line": 225, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11807,8 +12656,8 @@ "contract_id": "GiteaRepoCreateRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 231, - "end_line": 239, + "start_line": 228, + "end_line": 236, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11824,8 +12673,8 @@ "contract_id": "RemoteRepoSchema", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 242, - "end_line": 253, + "start_line": 239, + "end_line": 250, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11841,8 +12690,8 @@ "contract_id": "RemoteRepoCreateRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 256, - "end_line": 264, + "start_line": 253, + "end_line": 261, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11858,8 +12707,8 @@ "contract_id": "PromoteRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 267, - "end_line": 278, + "start_line": 264, + "end_line": 275, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -11875,8 +12724,8 @@ "contract_id": "PromoteResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/git_schemas.py", - "start_line": 281, - "end_line": 291, + "start_line": 278, + "end_line": 288, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12406,7 +13255,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/maintenance/_routes.py", "start_line": 1, - "end_line": 592, + "end_line": 590, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -12444,14 +13293,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, routes, maintenance, api]\n# @BRIEF Route handler stubs for all 7 Maintenance Banner API endpoints with RBAC guards per FR-015.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [MaintenanceSchemasModule]\n# @RELATION DEPENDS_ON -> [MaintenanceRouter]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [EXT:frontend:MaintenanceServiceModule]\n# @INVARIANT All mutation endpoints return 202 {task_id} per spec.\n# @INVARIANT RBAC enforced per FR-015 matrix via has_permission() dependency.\n\nfrom datetime import datetime, timezone\nfrom typing import Any, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Request, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.logger import belief_scope, logger as app_logger\nfrom ....core.task_manager import TaskManager\n\napp_logger = cast(Any, app_logger)\nfrom ....dependencies import (\n check_api_key_environment_scope,\n get_db,\n get_task_manager,\n has_permission,\n require_api_key_or_jwt,\n)\nfrom ....models.api_key import APIKey\nfrom ....models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom ._router import router\nfrom ._schemas import (\n MaintenanceAlreadyActiveResponse,\n MaintenanceDashboardBannerState,\n MaintenanceEndResponse,\n MaintenanceEventItem,\n MaintenanceEventListResponse,\n MaintenanceSettingsResponse,\n MaintenanceSettingsUpdate,\n MaintenanceStartRequest,\n MaintenanceStartResponse,\n)\n\n\n# ── GET /api/maintenance/dashboard-banners ───────────────────\n# FR-015: viewer, operator, admin all have READ access\n\n# #region list_dashboard_banners [C:2] [TYPE Function]\n# @BRIEF Get per-dashboard banner state for the Dashboard Hub indicator.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/dashboard-banners\", response_model=list[MaintenanceDashboardBannerState])\nasync def list_dashboard_banners(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n):\n with belief_scope(\"list_dashboard_banners\"):\n # Query active banners with their linked active/pending states\n active_banners = (\n db.query(MaintenanceDashboardBanner)\n .filter(MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE)\n .all()\n )\n\n result: list[MaintenanceDashboardBannerState] = []\n for banner in active_banners:\n linked_states = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.banner_id == banner.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .all()\n )\n\n event_ids: list[str] = []\n start_times: list[str] = []\n end_times: list[str] = []\n\n for state in linked_states:\n event = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == state.event_id)\n .first()\n )\n if event:\n event_ids.append(event.id)\n if event.start_time:\n start_times.append(event.start_time.isoformat())\n if event.end_time:\n end_times.append(event.end_time.isoformat())\n\n result.append(\n MaintenanceDashboardBannerState(\n dashboard_id=banner.dashboard_id,\n active=True,\n events=event_ids,\n start_time=min(start_times) if start_times else None,\n end_time=max(end_times) if end_times else None,\n )\n )\n\n return result\n# #endregion list_dashboard_banners\n\n\n# ── GET /api/maintenance/events ──────────────────────────────\n# FR-015: operator, admin (NOT viewer)\n\n# #region list_events [C:2] [TYPE Function]\n# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/events\", response_model=MaintenanceEventListResponse)\nasync def list_events(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"list_events\"):\n # ── Auto-expiry: end events past their end_time ──\n now = datetime.now(timezone.utc)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n app_logger.reason(\n \"Auto-ending expired maintenance event\",\n extra={\"event_id\": ev.id, \"end_time\": ev.end_time.isoformat()},\n )\n # Schedule proper cleanup via task manager (removes banner from dashboard)\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n app_logger.reflect(\n \"Scheduled cleanup for expired event\",\n extra={\"event_id\": ev.id, \"task_id\": task.id},\n )\n\n # Active events: ACTIVE, PARTIAL (per spec T040)\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events: COMPLETED, FAILED\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(50)\n .all()\n )\n\n def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:\n affected_count = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == event.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n return MaintenanceEventItem(\n id=event.id,\n tables=list(event.tables) if event.tables else [],\n start_time=event.start_time.isoformat() if event.start_time else \"\",\n end_time=event.end_time.isoformat() if event.end_time else None,\n message=event.message or None,\n status=event.status.value,\n affected_count=affected_count,\n created_at=event.created_at.isoformat() if event.created_at else \"\",\n )\n\n return MaintenanceEventListResponse(\n active=[_to_item(e) for e in active_events],\n completed=[_to_item(e) for e in completed_events],\n )\n# #endregion list_events\n\n\n# ── POST /api/maintenance/start ──────────────────────────────\n# FR-001: Always returns 202 {task_id} after validation\n# FR-015: operator, admin (NOT viewer)\n\n# #region start_maintenance [C:3] [TYPE Function]\n# @BRIEF Start a maintenance event. Validates input, creates event, dispatches TaskManager task.\n# Always returns 202 with task_id and maintenance_id.\n# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).\n# Tables sorted & lowered for idempotency key. Message NOT in key.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n# @RELATION DEPENDS_ON -> [TaskManager]\n@router.post(\n \"/start\",\n status_code=status.HTTP_202_ACCEPTED,\n responses={\n 202: {\"model\": MaintenanceStartResponse},\n 409: {\"model\": MaintenanceAlreadyActiveResponse},\n },\n)\nasync def start_maintenance(\n request: MaintenanceStartRequest,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:start\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"start_maintenance\"):\n # ── Validation ──\n\n # Validate end_time > start_time\n if request.end_time and request.start_time and request.end_time <= request.start_time:\n raise HTTPException(\n status_code=400,\n detail=\"end_time must be after start_time\",\n )\n\n # Validate times are within reasonable range (future ±1h grace period)\n now = datetime.now(timezone.utc)\n grace_threshold = 3600 # 1 hour in seconds\n if request.start_time:\n diff = (request.start_time - now).total_seconds() if request.start_time.tzinfo else \\\n (request.start_time.replace(tzinfo=timezone.utc) - now).total_seconds()\n if diff < -grace_threshold:\n raise HTTPException(\n status_code=400,\n detail=\"start_time is too far in the past (max 1h grace period)\",\n )\n\n # Validate tables list length\n if len(request.tables) > 100:\n raise HTTPException(\n status_code=400,\n detail=\"Maximum 100 tables allowed\",\n )\n\n # Sanitize message (HTML escape already done in rendering, but check for raw HTML)\n message = request.message\n if message:\n import html as _html\n sanitized = _html.escape(message)\n if sanitized != message:\n # Message contained HTML — accept it, it will be escaped at render time\n pass\n\n # ── Idempotency check ──\n from ....services.maintenance import build_idempotency_key\n\n sorted_tables = sorted(t.strip().lower() for t in request.tables if t.strip())\n idem_tables = frozenset(sorted_tables)\n idem_start = request.start_time.isoformat() if request.start_time else None\n idem_end = request.end_time.isoformat() if request.end_time else None\n\n # Find existing active event with same idempotency key\n active_statuses = [\n MaintenanceEventStatus.PENDING,\n MaintenanceEventStatus.APPLYING,\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n existing = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_(active_statuses),\n )\n .all()\n )\n for ev in existing:\n ev_tables = frozenset(\n sorted(t.lower() for t in (ev.tables or []) if t)\n )\n ev_start = ev.start_time.isoformat() if ev.start_time else None\n ev_end = ev.end_time.isoformat() if ev.end_time else None\n if ev_tables == idem_tables and ev_start == idem_start and ev_end == idem_end:\n # Same key → return already_active\n app_logger.reflect(\n \"Idempotency hit — event already active\",\n extra={\"existing_id\": ev.id},\n )\n return MaintenanceAlreadyActiveResponse(\n maintenance_id=ev.id,\n status=\"already_active\",\n )\n\n # ── Environment scoping ──\n env_id = request.environment_id\n\n # ── Create event ──\n event = MaintenanceEvent(\n tables=sorted_tables,\n start_time=request.start_time,\n end_time=request.end_time,\n message=message,\n status=MaintenanceEventStatus.PENDING,\n environment_id=env_id,\n )\n db.add(event)\n db.flush()\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"event_id\": event.id,\n \"environment_id\": env_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n event.task_id = task.id\n db.commit()\n\n app_logger.reflect(\n \"Maintenance start dispatched\",\n extra={\"event_id\": event.id, \"task_id\": task.id},\n )\n\n return MaintenanceStartResponse(\n task_id=task.id,\n maintenance_id=event.id,\n status=\"pending\",\n )\n# #endregion start_maintenance\n\n\n# ── POST /api/maintenance/{maintenance_id}/end ───────────────\n# FR-006: Remove banners for a specific maintenance event\n# FR-015: operator, admin (NOT viewer)\n\n# #region end_maintenance [C:3] [TYPE Function]\n# @BRIEF End a specific maintenance event by ID. Removes banners from affected dashboards.\n# Always returns 202 with task_id. Idempotent: already_completed returns success.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n@router.post(\n \"/{maintenance_id}/end\",\n status_code=status.HTTP_202_ACCEPTED,\n response_model=MaintenanceEndResponse,\n)\nasync def end_maintenance(\n maintenance_id: str,\n request: Request,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:end\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"end_maintenance\", f\"maintenance_id={maintenance_id}\"):\n # Check event exists\n event = db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n\n if not event:\n raise HTTPException(\n status_code=404,\n detail=f\"Maintenance event {maintenance_id} not found\",\n )\n\n # Validate API key environment scope against event's environment\n if _auth.startswith(\"api_key:\"):\n await check_api_key_environment_scope(request, db, event.environment_id or \"\")\n\n # If already completed, return idempotent success\n if event.status == MaintenanceEventStatus.COMPLETED:\n app_logger.reflect(\n \"Event already completed, returning idempotent\",\n extra={\"event_id\": maintenance_id},\n )\n return MaintenanceEndResponse(\n task_id=event.task_id or \"\",\n status=\"already_completed\",\n )\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": maintenance_id,\n \"environment_id\": event.environment_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n\n app_logger.reflect(\n \"Maintenance end dispatched\",\n extra={\"event_id\": maintenance_id, \"task_id\": task.id},\n )\n\n return MaintenanceEndResponse(\n task_id=task.id,\n status=\"pending\",\n )\n# #endregion end_maintenance\n\n\n# ── POST /api/maintenance/end-all ────────────────────────────\n# FR-007: Bulk removal of all active banners\n# FR-015: operator, admin (NOT viewer)\n\n# #region end_all_maintenance [C:3] [TYPE Function]\n# @BRIEF End all active maintenance events. Removes all banners from all dashboards.\n# Always returns 202 with task_id. Supports environment scoping for API keys.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n@router.post(\n \"/end-all\",\n status_code=status.HTTP_202_ACCEPTED,\n response_model=MaintenanceEndResponse,\n)\nasync def end_all_maintenance(\n request: Request,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:end_all\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"end_all_maintenance\"):\n # ── Resolve environment scope ──\n effective_env_id: str | None = None\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except Exception:\n pass\n\n # ── Enforce API key environment scoping ──\n if _auth.startswith(\"api_key:\"):\n # Auto-scope to the key's environment if not specified in body\n if not effective_env_id:\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n from ....core.auth.api_key import hash_api_key\n key_hash = hash_api_key(raw_key)\n key_row = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if key_row and key_row.environment_id:\n effective_env_id = key_row.environment_id\n\n await check_api_key_environment_scope(request, db, effective_env_id or \"\")\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end_all\",\n \"environment_id\": effective_env_id or \"\",\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n\n app_logger.reflect(\n \"End-all maintenance dispatched\",\n extra={\"task_id\": task.id, \"environment_id\": effective_env_id},\n )\n\n return MaintenanceEndResponse(\n task_id=task.id,\n status=\"pending\",\n )\n# #endregion end_all_maintenance\n\n\n# ── GET /api/maintenance/settings ────────────────────────────\n# FR-009: Get maintenance settings configuration\n# FR-015: operator, admin (NOT viewer)\n\n# #region get_maintenance_settings [C:2] [TYPE Function]\n# @BRIEF Get current maintenance settings configuration.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/settings\", response_model=MaintenanceSettingsResponse)\nasync def get_maintenance_settings(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n):\n with belief_scope(\"get_maintenance_settings\"):\n settings = db.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n\n if not settings:\n raise HTTPException(\n status_code=404,\n detail=\"Maintenance settings not configured\",\n )\n\n return MaintenanceSettingsResponse(\n target_environment_id=settings.target_environment_id,\n display_timezone=settings.display_timezone,\n banner_template=settings.banner_template,\n dashboard_scope=settings.dashboard_scope.value,\n excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),\n forced_dashboard_ids=list(settings.forced_dashboard_ids or []),\n updated_at=settings.updated_at.isoformat() if settings.updated_at else None,\n )\n# #endregion get_maintenance_settings\n\n\n# ── PUT /api/maintenance/settings ────────────────────────────\n# FR-009: Update maintenance settings configuration\n# FR-015: admin ONLY (NOT operator, NOT viewer)\n\n# #region update_maintenance_settings [C:2] [TYPE Function]\n# @BRIEF Update maintenance settings. All fields optional for partial update.\n# admin role required per FR-015.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"admin:settings\", \"WRITE\")]\n@router.put(\"/settings\", response_model=MaintenanceSettingsResponse)\nasync def update_maintenance_settings(\n settings_data: MaintenanceSettingsUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_maintenance_settings\"):\n settings = db.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n\n if not settings:\n # Auto-create with defaults on first save (upsert behavior).\n # Column-level defaults from the model apply for omitted fields.\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=settings_data.target_environment_id or \"\",\n )\n db.add(settings)\n db.flush()\n\n # Apply partial updates\n update_data = settings_data.model_dump(exclude_unset=True)\n\n if \"target_environment_id\" in update_data:\n settings.target_environment_id = update_data[\"target_environment_id\"]\n if \"display_timezone\" in update_data:\n settings.display_timezone = update_data[\"display_timezone\"]\n if \"banner_template\" in update_data:\n settings.banner_template = update_data[\"banner_template\"]\n if \"dashboard_scope\" in update_data:\n from ....models.maintenance import DashboardScope\n settings.dashboard_scope = DashboardScope(update_data[\"dashboard_scope\"])\n if \"excluded_dashboard_ids\" in update_data:\n settings.excluded_dashboard_ids = update_data[\"excluded_dashboard_ids\"]\n if \"forced_dashboard_ids\" in update_data:\n settings.forced_dashboard_ids = update_data[\"forced_dashboard_ids\"]\n\n db.commit()\n db.refresh(settings)\n\n return MaintenanceSettingsResponse(\n target_environment_id=settings.target_environment_id,\n display_timezone=settings.display_timezone,\n banner_template=settings.banner_template,\n dashboard_scope=settings.dashboard_scope.value,\n excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),\n forced_dashboard_ids=list(settings.forced_dashboard_ids or []),\n updated_at=settings.updated_at.isoformat() if settings.updated_at else None,\n )\n# #endregion update_maintenance_settings\n\n# #endregion MaintenanceRoutesModule\n" + "body": "# #region MaintenanceRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, routes, maintenance, api]\n# @BRIEF Route handler stubs for all 7 Maintenance Banner API endpoints with RBAC guards per FR-015.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [MaintenanceSchemasModule]\n# @RELATION DEPENDS_ON -> [MaintenanceRouter]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [EXT:frontend:MaintenanceServiceModule]\n# @INVARIANT All mutation endpoints return 202 {task_id} per spec.\n# @INVARIANT RBAC enforced per FR-015 matrix via has_permission() dependency.\n\nfrom datetime import UTC, datetime\nfrom typing import Any, cast\n\nfrom fastapi import Depends, HTTPException, Request, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.logger import belief_scope, logger as app_logger\nfrom ....core.task_manager import TaskManager\n\napp_logger = cast(Any, app_logger)\nfrom ....dependencies import (\n check_api_key_environment_scope,\n get_db,\n get_task_manager,\n has_permission,\n require_api_key_or_jwt,\n)\nfrom ....models.api_key import APIKey\nfrom ....models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom ._router import router\nfrom ._schemas import (\n MaintenanceAlreadyActiveResponse,\n MaintenanceDashboardBannerState,\n MaintenanceEndResponse,\n MaintenanceEventItem,\n MaintenanceEventListResponse,\n MaintenanceSettingsResponse,\n MaintenanceSettingsUpdate,\n MaintenanceStartRequest,\n MaintenanceStartResponse,\n)\n\n# ── GET /api/maintenance/dashboard-banners ───────────────────\n# FR-015: viewer, operator, admin all have READ access\n\n# #region list_dashboard_banners [C:2] [TYPE Function]\n# @BRIEF Get per-dashboard banner state for the Dashboard Hub indicator.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/dashboard-banners\", response_model=list[MaintenanceDashboardBannerState])\nasync def list_dashboard_banners(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n):\n with belief_scope(\"list_dashboard_banners\"):\n # Query active banners with their linked active/pending states\n active_banners = (\n db.query(MaintenanceDashboardBanner)\n .filter(MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE)\n .all()\n )\n\n result: list[MaintenanceDashboardBannerState] = []\n for banner in active_banners:\n linked_states = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.banner_id == banner.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .all()\n )\n\n event_ids: list[str] = []\n start_times: list[str] = []\n end_times: list[str] = []\n\n for state in linked_states:\n event = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == state.event_id)\n .first()\n )\n if event:\n event_ids.append(event.id)\n if event.start_time:\n start_times.append(event.start_time.isoformat())\n if event.end_time:\n end_times.append(event.end_time.isoformat())\n\n result.append(\n MaintenanceDashboardBannerState(\n dashboard_id=banner.dashboard_id,\n active=True,\n events=event_ids,\n start_time=min(start_times) if start_times else None,\n end_time=max(end_times) if end_times else None,\n )\n )\n\n return result\n# #endregion list_dashboard_banners\n\n\n# ── GET /api/maintenance/events ──────────────────────────────\n# FR-015: operator, admin (NOT viewer)\n\n# #region list_events [C:2] [TYPE Function]\n# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/events\", response_model=MaintenanceEventListResponse)\nasync def list_events(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"list_events\"):\n # ── Auto-expiry: end events past their end_time ──\n now = datetime.now(UTC)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n app_logger.reason(\n \"Auto-ending expired maintenance event\",\n extra={\"event_id\": ev.id, \"end_time\": ev.end_time.isoformat()},\n )\n # Schedule proper cleanup via task manager (removes banner from dashboard)\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n app_logger.reflect(\n \"Scheduled cleanup for expired event\",\n extra={\"event_id\": ev.id, \"task_id\": task.id},\n )\n\n # Active events: ACTIVE, PARTIAL (per spec T040)\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events: COMPLETED, FAILED\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(50)\n .all()\n )\n\n def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:\n affected_count = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == event.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n return MaintenanceEventItem(\n id=event.id,\n tables=list(event.tables) if event.tables else [],\n start_time=event.start_time.isoformat() if event.start_time else \"\",\n end_time=event.end_time.isoformat() if event.end_time else None,\n message=event.message or None,\n status=event.status.value,\n affected_count=affected_count,\n created_at=event.created_at.isoformat() if event.created_at else \"\",\n )\n\n return MaintenanceEventListResponse(\n active=[_to_item(e) for e in active_events],\n completed=[_to_item(e) for e in completed_events],\n )\n# #endregion list_events\n\n\n# ── POST /api/maintenance/start ──────────────────────────────\n# FR-001: Always returns 202 {task_id} after validation\n# FR-015: operator, admin (NOT viewer)\n\n# #region start_maintenance [C:3] [TYPE Function]\n# @BRIEF Start a maintenance event. Validates input, creates event, dispatches TaskManager task.\n# Always returns 202 with task_id and maintenance_id.\n# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).\n# Tables sorted & lowered for idempotency key. Message NOT in key.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n# @RELATION DEPENDS_ON -> [TaskManager]\n@router.post(\n \"/start\",\n status_code=status.HTTP_202_ACCEPTED,\n responses={\n 202: {\"model\": MaintenanceStartResponse},\n 409: {\"model\": MaintenanceAlreadyActiveResponse},\n },\n)\nasync def start_maintenance(\n request: MaintenanceStartRequest,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:start\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"start_maintenance\"):\n # ── Validation ──\n\n # Validate end_time > start_time\n if request.end_time and request.start_time and request.end_time <= request.start_time:\n raise HTTPException(\n status_code=400,\n detail=\"end_time must be after start_time\",\n )\n\n # Validate times are within reasonable range (future ±1h grace period)\n now = datetime.now(UTC)\n grace_threshold = 3600 # 1 hour in seconds\n if request.start_time:\n diff = (request.start_time - now).total_seconds() if request.start_time.tzinfo else \\\n (request.start_time.replace(tzinfo=UTC) - now).total_seconds()\n if diff < -grace_threshold:\n raise HTTPException(\n status_code=400,\n detail=\"start_time is too far in the past (max 1h grace period)\",\n )\n\n # Validate tables list length\n if len(request.tables) > 100:\n raise HTTPException(\n status_code=400,\n detail=\"Maximum 100 tables allowed\",\n )\n\n # Sanitize message (HTML escape already done in rendering, but check for raw HTML)\n message = request.message\n if message:\n import html as _html\n sanitized = _html.escape(message)\n if sanitized != message:\n # Message contained HTML — accept it, it will be escaped at render time\n pass\n\n # ── Idempotency check ──\n\n sorted_tables = sorted(t.strip().lower() for t in request.tables if t.strip())\n idem_tables = frozenset(sorted_tables)\n idem_start = request.start_time.isoformat() if request.start_time else None\n idem_end = request.end_time.isoformat() if request.end_time else None\n\n # Find existing active event with same idempotency key\n active_statuses = [\n MaintenanceEventStatus.PENDING,\n MaintenanceEventStatus.APPLYING,\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n existing = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_(active_statuses),\n )\n .all()\n )\n for ev in existing:\n ev_tables = frozenset(\n sorted(t.lower() for t in (ev.tables or []) if t)\n )\n ev_start = ev.start_time.isoformat() if ev.start_time else None\n ev_end = ev.end_time.isoformat() if ev.end_time else None\n if ev_tables == idem_tables and ev_start == idem_start and ev_end == idem_end:\n # Same key → return already_active\n app_logger.reflect(\n \"Idempotency hit — event already active\",\n extra={\"existing_id\": ev.id},\n )\n return MaintenanceAlreadyActiveResponse(\n maintenance_id=ev.id,\n status=\"already_active\",\n )\n\n # ── Environment scoping ──\n env_id = request.environment_id\n\n # ── Create event ──\n event = MaintenanceEvent(\n tables=sorted_tables,\n start_time=request.start_time,\n end_time=request.end_time,\n message=message,\n status=MaintenanceEventStatus.PENDING,\n environment_id=env_id,\n )\n db.add(event)\n db.flush()\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"event_id\": event.id,\n \"environment_id\": env_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n event.task_id = task.id\n db.commit()\n\n app_logger.reflect(\n \"Maintenance start dispatched\",\n extra={\"event_id\": event.id, \"task_id\": task.id},\n )\n\n return MaintenanceStartResponse(\n task_id=task.id,\n maintenance_id=event.id,\n status=\"pending\",\n )\n# #endregion start_maintenance\n\n\n# ── POST /api/maintenance/{maintenance_id}/end ───────────────\n# FR-006: Remove banners for a specific maintenance event\n# FR-015: operator, admin (NOT viewer)\n\n# #region end_maintenance [C:3] [TYPE Function]\n# @BRIEF End a specific maintenance event by ID. Removes banners from affected dashboards.\n# Always returns 202 with task_id. Idempotent: already_completed returns success.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n@router.post(\n \"/{maintenance_id}/end\",\n status_code=status.HTTP_202_ACCEPTED,\n response_model=MaintenanceEndResponse,\n)\nasync def end_maintenance(\n maintenance_id: str,\n request: Request,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:end\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"end_maintenance\", f\"maintenance_id={maintenance_id}\"):\n # Check event exists\n event = db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n\n if not event:\n raise HTTPException(\n status_code=404,\n detail=f\"Maintenance event {maintenance_id} not found\",\n )\n\n # Validate API key environment scope against event's environment\n if _auth.startswith(\"api_key:\"):\n await check_api_key_environment_scope(request, db, event.environment_id or \"\")\n\n # If already completed, return idempotent success\n if event.status == MaintenanceEventStatus.COMPLETED:\n app_logger.reflect(\n \"Event already completed, returning idempotent\",\n extra={\"event_id\": maintenance_id},\n )\n return MaintenanceEndResponse(\n task_id=event.task_id or \"\",\n status=\"already_completed\",\n )\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": maintenance_id,\n \"environment_id\": event.environment_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n\n app_logger.reflect(\n \"Maintenance end dispatched\",\n extra={\"event_id\": maintenance_id, \"task_id\": task.id},\n )\n\n return MaintenanceEndResponse(\n task_id=task.id,\n status=\"pending\",\n )\n# #endregion end_maintenance\n\n\n# ── POST /api/maintenance/end-all ────────────────────────────\n# FR-007: Bulk removal of all active banners\n# FR-015: operator, admin (NOT viewer)\n\n# #region end_all_maintenance [C:3] [TYPE Function]\n# @BRIEF End all active maintenance events. Removes all banners from all dashboards.\n# Always returns 202 with task_id. Supports environment scoping for API keys.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n@router.post(\n \"/end-all\",\n status_code=status.HTTP_202_ACCEPTED,\n response_model=MaintenanceEndResponse,\n)\nasync def end_all_maintenance(\n request: Request,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:end_all\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"end_all_maintenance\"):\n # ── Resolve environment scope ──\n effective_env_id: str | None = None\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except Exception:\n pass\n\n # ── Enforce API key environment scoping ──\n if _auth.startswith(\"api_key:\"):\n # Auto-scope to the key's environment if not specified in body\n if not effective_env_id:\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n from ....core.auth.api_key import hash_api_key\n key_hash = hash_api_key(raw_key)\n key_row = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if key_row and key_row.environment_id:\n effective_env_id = key_row.environment_id\n\n await check_api_key_environment_scope(request, db, effective_env_id or \"\")\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end_all\",\n \"environment_id\": effective_env_id or \"\",\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n\n app_logger.reflect(\n \"End-all maintenance dispatched\",\n extra={\"task_id\": task.id, \"environment_id\": effective_env_id},\n )\n\n return MaintenanceEndResponse(\n task_id=task.id,\n status=\"pending\",\n )\n# #endregion end_all_maintenance\n\n\n# ── GET /api/maintenance/settings ────────────────────────────\n# FR-009: Get maintenance settings configuration\n# FR-015: operator, admin (NOT viewer)\n\n# #region get_maintenance_settings [C:2] [TYPE Function]\n# @BRIEF Get current maintenance settings configuration.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/settings\", response_model=MaintenanceSettingsResponse)\nasync def get_maintenance_settings(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n):\n with belief_scope(\"get_maintenance_settings\"):\n settings = db.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n\n if not settings:\n raise HTTPException(\n status_code=404,\n detail=\"Maintenance settings not configured\",\n )\n\n return MaintenanceSettingsResponse(\n target_environment_id=settings.target_environment_id,\n display_timezone=settings.display_timezone,\n banner_template=settings.banner_template,\n dashboard_scope=settings.dashboard_scope.value,\n excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),\n forced_dashboard_ids=list(settings.forced_dashboard_ids or []),\n updated_at=settings.updated_at.isoformat() if settings.updated_at else None,\n )\n# #endregion get_maintenance_settings\n\n\n# ── PUT /api/maintenance/settings ────────────────────────────\n# FR-009: Update maintenance settings configuration\n# FR-015: admin ONLY (NOT operator, NOT viewer)\n\n# #region update_maintenance_settings [C:2] [TYPE Function]\n# @BRIEF Update maintenance settings. All fields optional for partial update.\n# admin role required per FR-015.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"admin:settings\", \"WRITE\")]\n@router.put(\"/settings\", response_model=MaintenanceSettingsResponse)\nasync def update_maintenance_settings(\n settings_data: MaintenanceSettingsUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_maintenance_settings\"):\n settings = db.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n\n if not settings:\n # Auto-create with defaults on first save (upsert behavior).\n # Column-level defaults from the model apply for omitted fields.\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=settings_data.target_environment_id or \"\",\n )\n db.add(settings)\n db.flush()\n\n # Apply partial updates\n update_data = settings_data.model_dump(exclude_unset=True)\n\n if \"target_environment_id\" in update_data:\n settings.target_environment_id = update_data[\"target_environment_id\"]\n if \"display_timezone\" in update_data:\n settings.display_timezone = update_data[\"display_timezone\"]\n if \"banner_template\" in update_data:\n settings.banner_template = update_data[\"banner_template\"]\n if \"dashboard_scope\" in update_data:\n from ....models.maintenance import DashboardScope\n settings.dashboard_scope = DashboardScope(update_data[\"dashboard_scope\"])\n if \"excluded_dashboard_ids\" in update_data:\n settings.excluded_dashboard_ids = update_data[\"excluded_dashboard_ids\"]\n if \"forced_dashboard_ids\" in update_data:\n settings.forced_dashboard_ids = update_data[\"forced_dashboard_ids\"]\n\n db.commit()\n db.refresh(settings)\n\n return MaintenanceSettingsResponse(\n target_environment_id=settings.target_environment_id,\n display_timezone=settings.display_timezone,\n banner_template=settings.banner_template,\n dashboard_scope=settings.dashboard_scope.value,\n excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),\n forced_dashboard_ids=list(settings.forced_dashboard_ids or []),\n updated_at=settings.updated_at.isoformat() if settings.updated_at else None,\n )\n# #endregion update_maintenance_settings\n\n# #endregion MaintenanceRoutesModule\n" }, { "contract_id": "list_dashboard_banners", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 55, - "end_line": 113, + "start_line": 54, + "end_line": 112, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -12475,8 +13324,8 @@ "contract_id": "list_events", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 119, - "end_line": 217, + "start_line": 118, + "end_line": 216, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -12494,14 +13343,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region list_events [C:2] [TYPE Function]\n# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/events\", response_model=MaintenanceEventListResponse)\nasync def list_events(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"list_events\"):\n # ── Auto-expiry: end events past their end_time ──\n now = datetime.now(timezone.utc)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n app_logger.reason(\n \"Auto-ending expired maintenance event\",\n extra={\"event_id\": ev.id, \"end_time\": ev.end_time.isoformat()},\n )\n # Schedule proper cleanup via task manager (removes banner from dashboard)\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n app_logger.reflect(\n \"Scheduled cleanup for expired event\",\n extra={\"event_id\": ev.id, \"task_id\": task.id},\n )\n\n # Active events: ACTIVE, PARTIAL (per spec T040)\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events: COMPLETED, FAILED\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(50)\n .all()\n )\n\n def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:\n affected_count = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == event.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n return MaintenanceEventItem(\n id=event.id,\n tables=list(event.tables) if event.tables else [],\n start_time=event.start_time.isoformat() if event.start_time else \"\",\n end_time=event.end_time.isoformat() if event.end_time else None,\n message=event.message or None,\n status=event.status.value,\n affected_count=affected_count,\n created_at=event.created_at.isoformat() if event.created_at else \"\",\n )\n\n return MaintenanceEventListResponse(\n active=[_to_item(e) for e in active_events],\n completed=[_to_item(e) for e in completed_events],\n )\n# #endregion list_events\n" + "body": "# #region list_events [C:2] [TYPE Function]\n# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]\n@router.get(\"/events\", response_model=MaintenanceEventListResponse)\nasync def list_events(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"maintenance\", \"READ\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"list_events\"):\n # ── Auto-expiry: end events past their end_time ──\n now = datetime.now(UTC)\n expired = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.ENDING,\n ]),\n MaintenanceEvent.end_time.isnot(None),\n MaintenanceEvent.end_time < now,\n )\n .all()\n )\n for ev in expired:\n app_logger.reason(\n \"Auto-ending expired maintenance event\",\n extra={\"event_id\": ev.id, \"end_time\": ev.end_time.isoformat()},\n )\n # Schedule proper cleanup via task manager (removes banner from dashboard)\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": ev.id,\n \"environment_id\": ev.environment_id,\n \"user\": \"system\",\n },\n )\n app_logger.reflect(\n \"Scheduled cleanup for expired event\",\n extra={\"event_id\": ev.id, \"task_id\": task.id},\n )\n\n # Active events: ACTIVE, PARTIAL (per spec T040)\n active_statuses = [\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n active_events = (\n db.query(MaintenanceEvent)\n .filter(MaintenanceEvent.status.in_(active_statuses))\n .order_by(MaintenanceEvent.created_at.desc())\n .all()\n )\n\n # Completed events: COMPLETED, FAILED\n completed_events = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.COMPLETED,\n MaintenanceEventStatus.FAILED,\n ])\n )\n .order_by(MaintenanceEvent.created_at.desc())\n .limit(50)\n .all()\n )\n\n def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:\n affected_count = (\n db.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.event_id == event.id,\n MaintenanceDashboardState.status.in_([\n MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardStateStatus.PENDING_APPLY,\n ]),\n )\n .count()\n )\n return MaintenanceEventItem(\n id=event.id,\n tables=list(event.tables) if event.tables else [],\n start_time=event.start_time.isoformat() if event.start_time else \"\",\n end_time=event.end_time.isoformat() if event.end_time else None,\n message=event.message or None,\n status=event.status.value,\n affected_count=affected_count,\n created_at=event.created_at.isoformat() if event.created_at else \"\",\n )\n\n return MaintenanceEventListResponse(\n active=[_to_item(e) for e in active_events],\n completed=[_to_item(e) for e in completed_events],\n )\n# #endregion list_events\n" }, { "contract_id": "start_maintenance", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 224, - "end_line": 360, + "start_line": 223, + "end_line": 358, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -12512,14 +13361,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region start_maintenance [C:3] [TYPE Function]\n# @BRIEF Start a maintenance event. Validates input, creates event, dispatches TaskManager task.\n# Always returns 202 with task_id and maintenance_id.\n# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).\n# Tables sorted & lowered for idempotency key. Message NOT in key.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n# @RELATION DEPENDS_ON -> [TaskManager]\n@router.post(\n \"/start\",\n status_code=status.HTTP_202_ACCEPTED,\n responses={\n 202: {\"model\": MaintenanceStartResponse},\n 409: {\"model\": MaintenanceAlreadyActiveResponse},\n },\n)\nasync def start_maintenance(\n request: MaintenanceStartRequest,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:start\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"start_maintenance\"):\n # ── Validation ──\n\n # Validate end_time > start_time\n if request.end_time and request.start_time and request.end_time <= request.start_time:\n raise HTTPException(\n status_code=400,\n detail=\"end_time must be after start_time\",\n )\n\n # Validate times are within reasonable range (future ±1h grace period)\n now = datetime.now(timezone.utc)\n grace_threshold = 3600 # 1 hour in seconds\n if request.start_time:\n diff = (request.start_time - now).total_seconds() if request.start_time.tzinfo else \\\n (request.start_time.replace(tzinfo=timezone.utc) - now).total_seconds()\n if diff < -grace_threshold:\n raise HTTPException(\n status_code=400,\n detail=\"start_time is too far in the past (max 1h grace period)\",\n )\n\n # Validate tables list length\n if len(request.tables) > 100:\n raise HTTPException(\n status_code=400,\n detail=\"Maximum 100 tables allowed\",\n )\n\n # Sanitize message (HTML escape already done in rendering, but check for raw HTML)\n message = request.message\n if message:\n import html as _html\n sanitized = _html.escape(message)\n if sanitized != message:\n # Message contained HTML — accept it, it will be escaped at render time\n pass\n\n # ── Idempotency check ──\n from ....services.maintenance import build_idempotency_key\n\n sorted_tables = sorted(t.strip().lower() for t in request.tables if t.strip())\n idem_tables = frozenset(sorted_tables)\n idem_start = request.start_time.isoformat() if request.start_time else None\n idem_end = request.end_time.isoformat() if request.end_time else None\n\n # Find existing active event with same idempotency key\n active_statuses = [\n MaintenanceEventStatus.PENDING,\n MaintenanceEventStatus.APPLYING,\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n existing = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_(active_statuses),\n )\n .all()\n )\n for ev in existing:\n ev_tables = frozenset(\n sorted(t.lower() for t in (ev.tables or []) if t)\n )\n ev_start = ev.start_time.isoformat() if ev.start_time else None\n ev_end = ev.end_time.isoformat() if ev.end_time else None\n if ev_tables == idem_tables and ev_start == idem_start and ev_end == idem_end:\n # Same key → return already_active\n app_logger.reflect(\n \"Idempotency hit — event already active\",\n extra={\"existing_id\": ev.id},\n )\n return MaintenanceAlreadyActiveResponse(\n maintenance_id=ev.id,\n status=\"already_active\",\n )\n\n # ── Environment scoping ──\n env_id = request.environment_id\n\n # ── Create event ──\n event = MaintenanceEvent(\n tables=sorted_tables,\n start_time=request.start_time,\n end_time=request.end_time,\n message=message,\n status=MaintenanceEventStatus.PENDING,\n environment_id=env_id,\n )\n db.add(event)\n db.flush()\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"event_id\": event.id,\n \"environment_id\": env_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n event.task_id = task.id\n db.commit()\n\n app_logger.reflect(\n \"Maintenance start dispatched\",\n extra={\"event_id\": event.id, \"task_id\": task.id},\n )\n\n return MaintenanceStartResponse(\n task_id=task.id,\n maintenance_id=event.id,\n status=\"pending\",\n )\n# #endregion start_maintenance\n" + "body": "# #region start_maintenance [C:3] [TYPE Function]\n# @BRIEF Start a maintenance event. Validates input, creates event, dispatches TaskManager task.\n# Always returns 202 with task_id and maintenance_id.\n# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).\n# Tables sorted & lowered for idempotency key. Message NOT in key.\n# @RELATION DEPENDS_ON -> [EXT:code:has_permission(\"maintenance\", \"WRITE\")]\n# @RELATION DEPENDS_ON -> [TaskManager]\n@router.post(\n \"/start\",\n status_code=status.HTTP_202_ACCEPTED,\n responses={\n 202: {\"model\": MaintenanceStartResponse},\n 409: {\"model\": MaintenanceAlreadyActiveResponse},\n },\n)\nasync def start_maintenance(\n request: MaintenanceStartRequest,\n db: Session = Depends(get_db),\n _auth=Depends(require_api_key_or_jwt(\"maintenance:start\", \"maintenance\", \"WRITE\")),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n with belief_scope(\"start_maintenance\"):\n # ── Validation ──\n\n # Validate end_time > start_time\n if request.end_time and request.start_time and request.end_time <= request.start_time:\n raise HTTPException(\n status_code=400,\n detail=\"end_time must be after start_time\",\n )\n\n # Validate times are within reasonable range (future ±1h grace period)\n now = datetime.now(UTC)\n grace_threshold = 3600 # 1 hour in seconds\n if request.start_time:\n diff = (request.start_time - now).total_seconds() if request.start_time.tzinfo else \\\n (request.start_time.replace(tzinfo=UTC) - now).total_seconds()\n if diff < -grace_threshold:\n raise HTTPException(\n status_code=400,\n detail=\"start_time is too far in the past (max 1h grace period)\",\n )\n\n # Validate tables list length\n if len(request.tables) > 100:\n raise HTTPException(\n status_code=400,\n detail=\"Maximum 100 tables allowed\",\n )\n\n # Sanitize message (HTML escape already done in rendering, but check for raw HTML)\n message = request.message\n if message:\n import html as _html\n sanitized = _html.escape(message)\n if sanitized != message:\n # Message contained HTML — accept it, it will be escaped at render time\n pass\n\n # ── Idempotency check ──\n\n sorted_tables = sorted(t.strip().lower() for t in request.tables if t.strip())\n idem_tables = frozenset(sorted_tables)\n idem_start = request.start_time.isoformat() if request.start_time else None\n idem_end = request.end_time.isoformat() if request.end_time else None\n\n # Find existing active event with same idempotency key\n active_statuses = [\n MaintenanceEventStatus.PENDING,\n MaintenanceEventStatus.APPLYING,\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n ]\n existing = (\n db.query(MaintenanceEvent)\n .filter(\n MaintenanceEvent.status.in_(active_statuses),\n )\n .all()\n )\n for ev in existing:\n ev_tables = frozenset(\n sorted(t.lower() for t in (ev.tables or []) if t)\n )\n ev_start = ev.start_time.isoformat() if ev.start_time else None\n ev_end = ev.end_time.isoformat() if ev.end_time else None\n if ev_tables == idem_tables and ev_start == idem_start and ev_end == idem_end:\n # Same key → return already_active\n app_logger.reflect(\n \"Idempotency hit — event already active\",\n extra={\"existing_id\": ev.id},\n )\n return MaintenanceAlreadyActiveResponse(\n maintenance_id=ev.id,\n status=\"already_active\",\n )\n\n # ── Environment scoping ──\n env_id = request.environment_id\n\n # ── Create event ──\n event = MaintenanceEvent(\n tables=sorted_tables,\n start_time=request.start_time,\n end_time=request.end_time,\n message=message,\n status=MaintenanceEventStatus.PENDING,\n environment_id=env_id,\n )\n db.add(event)\n db.flush()\n\n # ── Dispatch TaskManager task ──\n task = await task_manager.create_task(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"start\",\n \"event_id\": event.id,\n \"environment_id\": env_id,\n \"user\": _auth.split(\":\", 1)[1] if \":\" in _auth else _auth,\n },\n )\n event.task_id = task.id\n db.commit()\n\n app_logger.reflect(\n \"Maintenance start dispatched\",\n extra={\"event_id\": event.id, \"task_id\": task.id},\n )\n\n return MaintenanceStartResponse(\n task_id=task.id,\n maintenance_id=event.id,\n status=\"pending\",\n )\n# #endregion start_maintenance\n" }, { "contract_id": "end_maintenance", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 367, - "end_line": 430, + "start_line": 365, + "end_line": 428, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -12536,8 +13385,8 @@ "contract_id": "end_all_maintenance", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 437, - "end_line": 494, + "start_line": 435, + "end_line": 492, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -12554,8 +13403,8 @@ "contract_id": "get_maintenance_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 501, - "end_line": 529, + "start_line": 499, + "end_line": 527, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -12579,8 +13428,8 @@ "contract_id": "update_maintenance_settings", "contract_type": "Function", "file_path": "backend/src/api/routes/maintenance/_routes.py", - "start_line": 536, - "end_line": 590, + "start_line": 534, + "end_line": 588, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -12598,7 +13447,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/maintenance/_schemas.py", "start_line": 1, - "end_line": 157, + "end_line": 156, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -12618,14 +13467,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceSchemasModule [C:2] [TYPE Module] [SEMANTICS pydantic, schema, maintenance, request, response]\n# @BRIEF Pydantic models for Maintenance Banner API: request/response schemas per data-model.md.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n# @INVARIANT All response schemas use the standard envelope shape: { status, data, error, meta }\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n\n# ── Request Schemas ──────────────────────────────────────────\n\n# #region MaintenanceStartRequest [C:1] [TYPE Class]\n# @BRIEF POST /api/maintenance/start request body with environment scoping.\nclass MaintenanceStartRequest(BaseModel):\n tables: list[str] = Field(..., min_length=1, max_length=100)\n start_time: datetime\n end_time: datetime | None = None\n message: str | None = Field(None, max_length=500)\n environment_id: str = Field(..., description=\"Target environment for the maintenance operation\")\n# #endregion MaintenanceStartRequest\n\n\n# #region MaintenanceSettingsUpdate [C:1] [TYPE Class]\n# @BRIEF PUT /api/maintenance/settings request body — all fields optional for partial update.\nclass MaintenanceSettingsUpdate(BaseModel):\n target_environment_id: str | None = None\n display_timezone: str | None = None\n banner_template: str | None = None\n dashboard_scope: str | None = None\n excluded_dashboard_ids: list[int] | None = None\n forced_dashboard_ids: list[int] | None = None\n# #endregion MaintenanceSettingsUpdate\n\n\n# ── Response Schemas ─────────────────────────────────────────\n\n# #region MaintenanceStartResponse [C:1] [TYPE Class]\n# @BRIEF Response for /api/maintenance/start — always 202 with task_id.\nclass MaintenanceStartResponse(BaseModel):\n task_id: str\n maintenance_id: str\n status: str # \"pending\"\n# #endregion MaintenanceStartResponse\n\n\n# #region MaintenanceDashboardResult [C:1] [TYPE Class]\n# @BRIEF Describes a single dashboard affected by a maintenance operation.\nclass MaintenanceDashboardResult(BaseModel):\n id: int\n title: str\n chart_id: int | None = None\n# #endregion MaintenanceDashboardResult\n\n\n# #region MaintenanceFailedDashboard [C:1] [TYPE Class]\n# @BRIEF Describes a dashboard that failed during banner apply/removal.\nclass MaintenanceFailedDashboard(BaseModel):\n id: int\n title: str\n error: str\n# #endregion MaintenanceFailedDashboard\n\n\n# #region MaintenanceTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed maintenance start task.\nclass MaintenanceTaskResult(BaseModel):\n maintenance_id: str\n status: str # \"active\" | \"partial\" | \"completed\" | \"no_match\"\n affected_dashboards: int = 0\n dashboards: list[MaintenanceDashboardResult] = []\n failed_dashboards: list[MaintenanceFailedDashboard] = []\n unmatched_tables: list[str] = []\n# #endregion MaintenanceTaskResult\n\n\n# #region MaintenanceEndResponse [C:1] [TYPE Class]\n# @BRIEF Response for /api/maintenance/{id}/end and /api/maintenance/end-all — always 202 with task_id.\nclass MaintenanceEndResponse(BaseModel):\n task_id: str\n status: str # \"pending\"\n# #endregion MaintenanceEndResponse\n\n\n# #region MaintenanceEndTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed end maintenance task.\nclass MaintenanceEndTaskResult(BaseModel):\n maintenance_id: str\n removed_from: int = 0\n failed_removals: list[MaintenanceFailedDashboard] = []\n# #endregion MaintenanceEndTaskResult\n\n\n# #region MaintenanceEndAllTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed end-all maintenance task.\nclass MaintenanceEndAllTaskResult(BaseModel):\n closed_events: int = 0\n removed_from: int = 0\n failed_removals: list[MaintenanceFailedDashboard] = []\n# #endregion MaintenanceEndAllTaskResult\n\n\n# #region MaintenanceSettingsResponse [C:1] [TYPE Class]\n# @BRIEF Response for GET /api/maintenance/settings.\nclass MaintenanceSettingsResponse(BaseModel):\n target_environment_id: str\n display_timezone: str\n banner_template: str\n dashboard_scope: str\n excluded_dashboard_ids: list[int]\n forced_dashboard_ids: list[int]\n updated_at: str | None = None\n# #endregion MaintenanceSettingsResponse\n\n\n# #region MaintenanceEventItem [C:1] [TYPE Class]\n# @BRIEF Single maintenance event summary for GET /api/maintenance/events.\nclass MaintenanceEventItem(BaseModel):\n id: str\n tables: list[str]\n start_time: str\n end_time: str | None = None\n message: str | None = None\n status: str\n affected_count: int\n created_at: str\n# #endregion MaintenanceEventItem\n\n\n# #region MaintenanceEventListResponse [C:1] [TYPE Class]\n# @BRIEF Response for GET /api/maintenance/events.\nclass MaintenanceEventListResponse(BaseModel):\n active: list[MaintenanceEventItem]\n completed: list[MaintenanceEventItem]\n# #endregion MaintenanceEventListResponse\n\n\n# #region MaintenanceDashboardBannerState [C:1] [TYPE Class]\n# @BRIEF Per-dashboard banner state for GET /api/maintenance/dashboard-banners.\nclass MaintenanceDashboardBannerState(BaseModel):\n dashboard_id: int\n active: bool\n events: list[str]\n start_time: str | None = None\n end_time: str | None = None\n# #endregion MaintenanceDashboardBannerState\n\n\n# #region MaintenanceAlreadyActiveResponse [C:1] [TYPE Class]\n# @BRIEF Response for duplicate /start call — idempotency hit.\nclass MaintenanceAlreadyActiveResponse(BaseModel):\n maintenance_id: str\n status: str = \"already_active\"\n# #endregion MaintenanceAlreadyActiveResponse\n\n# #endregion MaintenanceSchemasModule\n" + "body": "# #region MaintenanceSchemasModule [C:2] [TYPE Module] [SEMANTICS pydantic, schema, maintenance, request, response]\n# @BRIEF Pydantic models for Maintenance Banner API: request/response schemas per data-model.md.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n# @INVARIANT All response schemas use the standard envelope shape: { status, data, error, meta }\n\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, Field\n\n# ── Request Schemas ──────────────────────────────────────────\n\n# #region MaintenanceStartRequest [C:1] [TYPE Class]\n# @BRIEF POST /api/maintenance/start request body with environment scoping.\nclass MaintenanceStartRequest(BaseModel):\n tables: list[str] = Field(..., min_length=1, max_length=100)\n start_time: datetime\n end_time: datetime | None = None\n message: str | None = Field(None, max_length=500)\n environment_id: str = Field(..., description=\"Target environment for the maintenance operation\")\n# #endregion MaintenanceStartRequest\n\n\n# #region MaintenanceSettingsUpdate [C:1] [TYPE Class]\n# @BRIEF PUT /api/maintenance/settings request body — all fields optional for partial update.\nclass MaintenanceSettingsUpdate(BaseModel):\n target_environment_id: str | None = None\n display_timezone: str | None = None\n banner_template: str | None = None\n dashboard_scope: str | None = None\n excluded_dashboard_ids: list[int] | None = None\n forced_dashboard_ids: list[int] | None = None\n# #endregion MaintenanceSettingsUpdate\n\n\n# ── Response Schemas ─────────────────────────────────────────\n\n# #region MaintenanceStartResponse [C:1] [TYPE Class]\n# @BRIEF Response for /api/maintenance/start — always 202 with task_id.\nclass MaintenanceStartResponse(BaseModel):\n task_id: str\n maintenance_id: str\n status: str # \"pending\"\n# #endregion MaintenanceStartResponse\n\n\n# #region MaintenanceDashboardResult [C:1] [TYPE Class]\n# @BRIEF Describes a single dashboard affected by a maintenance operation.\nclass MaintenanceDashboardResult(BaseModel):\n id: int\n title: str\n chart_id: int | None = None\n# #endregion MaintenanceDashboardResult\n\n\n# #region MaintenanceFailedDashboard [C:1] [TYPE Class]\n# @BRIEF Describes a dashboard that failed during banner apply/removal.\nclass MaintenanceFailedDashboard(BaseModel):\n id: int\n title: str\n error: str\n# #endregion MaintenanceFailedDashboard\n\n\n# #region MaintenanceTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed maintenance start task.\nclass MaintenanceTaskResult(BaseModel):\n maintenance_id: str\n status: str # \"active\" | \"partial\" | \"completed\" | \"no_match\"\n affected_dashboards: int = 0\n dashboards: list[MaintenanceDashboardResult] = []\n failed_dashboards: list[MaintenanceFailedDashboard] = []\n unmatched_tables: list[str] = []\n# #endregion MaintenanceTaskResult\n\n\n# #region MaintenanceEndResponse [C:1] [TYPE Class]\n# @BRIEF Response for /api/maintenance/{id}/end and /api/maintenance/end-all — always 202 with task_id.\nclass MaintenanceEndResponse(BaseModel):\n task_id: str\n status: str # \"pending\"\n# #endregion MaintenanceEndResponse\n\n\n# #region MaintenanceEndTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed end maintenance task.\nclass MaintenanceEndTaskResult(BaseModel):\n maintenance_id: str\n removed_from: int = 0\n failed_removals: list[MaintenanceFailedDashboard] = []\n# #endregion MaintenanceEndTaskResult\n\n\n# #region MaintenanceEndAllTaskResult [C:1] [TYPE Class]\n# @BRIEF Result payload for a completed end-all maintenance task.\nclass MaintenanceEndAllTaskResult(BaseModel):\n closed_events: int = 0\n removed_from: int = 0\n failed_removals: list[MaintenanceFailedDashboard] = []\n# #endregion MaintenanceEndAllTaskResult\n\n\n# #region MaintenanceSettingsResponse [C:1] [TYPE Class]\n# @BRIEF Response for GET /api/maintenance/settings.\nclass MaintenanceSettingsResponse(BaseModel):\n target_environment_id: str\n display_timezone: str\n banner_template: str\n dashboard_scope: str\n excluded_dashboard_ids: list[int]\n forced_dashboard_ids: list[int]\n updated_at: str | None = None\n# #endregion MaintenanceSettingsResponse\n\n\n# #region MaintenanceEventItem [C:1] [TYPE Class]\n# @BRIEF Single maintenance event summary for GET /api/maintenance/events.\nclass MaintenanceEventItem(BaseModel):\n id: str\n tables: list[str]\n start_time: str\n end_time: str | None = None\n message: str | None = None\n status: str\n affected_count: int\n created_at: str\n# #endregion MaintenanceEventItem\n\n\n# #region MaintenanceEventListResponse [C:1] [TYPE Class]\n# @BRIEF Response for GET /api/maintenance/events.\nclass MaintenanceEventListResponse(BaseModel):\n active: list[MaintenanceEventItem]\n completed: list[MaintenanceEventItem]\n# #endregion MaintenanceEventListResponse\n\n\n# #region MaintenanceDashboardBannerState [C:1] [TYPE Class]\n# @BRIEF Per-dashboard banner state for GET /api/maintenance/dashboard-banners.\nclass MaintenanceDashboardBannerState(BaseModel):\n dashboard_id: int\n active: bool\n events: list[str]\n start_time: str | None = None\n end_time: str | None = None\n# #endregion MaintenanceDashboardBannerState\n\n\n# #region MaintenanceAlreadyActiveResponse [C:1] [TYPE Class]\n# @BRIEF Response for duplicate /start call — idempotency hit.\nclass MaintenanceAlreadyActiveResponse(BaseModel):\n maintenance_id: str\n status: str = \"already_active\"\n# #endregion MaintenanceAlreadyActiveResponse\n\n# #endregion MaintenanceSchemasModule\n" }, { "contract_id": "MaintenanceStartRequest", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 14, - "end_line": 22, + "start_line": 13, + "end_line": 21, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12642,8 +13491,8 @@ "contract_id": "MaintenanceSettingsUpdate", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 25, - "end_line": 34, + "start_line": 24, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12660,8 +13509,8 @@ "contract_id": "MaintenanceStartResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 39, - "end_line": 45, + "start_line": 38, + "end_line": 44, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12678,8 +13527,8 @@ "contract_id": "MaintenanceDashboardResult", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 48, - "end_line": 54, + "start_line": 47, + "end_line": 53, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12696,8 +13545,8 @@ "contract_id": "MaintenanceFailedDashboard", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 57, - "end_line": 63, + "start_line": 56, + "end_line": 62, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12714,8 +13563,8 @@ "contract_id": "MaintenanceTaskResult", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 66, - "end_line": 75, + "start_line": 65, + "end_line": 74, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12732,8 +13581,8 @@ "contract_id": "MaintenanceEndResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 78, - "end_line": 83, + "start_line": 77, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12750,8 +13599,8 @@ "contract_id": "MaintenanceEndTaskResult", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 86, - "end_line": 92, + "start_line": 85, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12768,8 +13617,8 @@ "contract_id": "MaintenanceEndAllTaskResult", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 95, - "end_line": 101, + "start_line": 94, + "end_line": 100, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12786,8 +13635,8 @@ "contract_id": "MaintenanceSettingsResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 104, - "end_line": 114, + "start_line": 103, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12804,8 +13653,8 @@ "contract_id": "MaintenanceEventItem", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 117, - "end_line": 128, + "start_line": 116, + "end_line": 127, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12822,8 +13671,8 @@ "contract_id": "MaintenanceEventListResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 131, - "end_line": 136, + "start_line": 130, + "end_line": 135, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12840,8 +13689,8 @@ "contract_id": "MaintenanceDashboardBannerState", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 139, - "end_line": 147, + "start_line": 138, + "end_line": 146, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12858,8 +13707,8 @@ "contract_id": "MaintenanceAlreadyActiveResponse", "contract_type": "Class", "file_path": "backend/src/api/routes/maintenance/_schemas.py", - "start_line": 150, - "end_line": 155, + "start_line": 149, + "end_line": 154, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12877,7 +13726,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/mappings.py", "start_line": 1, - "end_line": 127, + "end_line": 126, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -12908,7 +13757,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS fastapi, mapping, api, mapping-create]\n#\n# @BRIEF API endpoints for managing database mappings and getting suggestions.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [mapping_service]\n#\n#\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.mapping import DatabaseMapping\n\nrouter = APIRouter(tags=[\"mappings\"])\n\n# #region MappingCreate [TYPE DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n# #endregion MappingCreate\n\n# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion MappingResponse\n\n# #region SuggestRequest [TYPE DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# #endregion SuggestRequest\n\n# #region get_mappings [TYPE Function]\n# @BRIEF List all saved database mappings.\n# @PRE db session is injected.\n# @POST Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=list[MappingResponse])\nasync def get_mappings(\n source_env_id: str | None = None,\n target_env_id: str | None = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# #endregion get_mappings\n\n# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE mapping is valid MappingCreate, db session is injected.\n# @POST DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n\n# #region suggest_mappings_api [TYPE Function]\n# @BRIEF Get suggested mappings based on fuzzy matching.\n# @PRE request is valid SuggestRequest, config_manager is injected.\n# @POST Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# #endregion suggest_mappings_api\n\n# #endregion MappingsApi\n" + "body": "# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS fastapi, mapping, api, mapping-create]\n#\n# @BRIEF API endpoints for managing database mappings and getting suggestions.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [mapping_service]\n#\n#\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel, ConfigDict\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.mapping import DatabaseMapping\n\nrouter = APIRouter(tags=[\"mappings\"])\n\n# #region MappingCreate [TYPE DataClass]\nclass MappingCreate(BaseModel):\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n# #endregion MappingCreate\n\n# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion MappingResponse\n\n# #region SuggestRequest [TYPE DataClass]\nclass SuggestRequest(BaseModel):\n source_env_id: str\n target_env_id: str\n# #endregion SuggestRequest\n\n# #region get_mappings [TYPE Function]\n# @BRIEF List all saved database mappings.\n# @PRE db session is injected.\n# @POST Returns filtered list of DatabaseMapping records.\n@router.get(\"\", response_model=list[MappingResponse])\nasync def get_mappings(\n source_env_id: str | None = None,\n target_env_id: str | None = None,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"get_mappings\"):\n query = db.query(DatabaseMapping)\n if source_env_id:\n query = query.filter(DatabaseMapping.source_env_id == source_env_id)\n if target_env_id:\n query = query.filter(DatabaseMapping.target_env_id == target_env_id)\n return query.all()\n# #endregion get_mappings\n\n# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE mapping is valid MappingCreate, db session is injected.\n# @POST DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.model_dump())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n\n# #region suggest_mappings_api [TYPE Function]\n# @BRIEF Get suggested mappings based on fuzzy matching.\n# @PRE request is valid SuggestRequest, config_manager is injected.\n# @POST Returns mapping suggestions.\n@router.post(\"/suggest\")\nasync def suggest_mappings_api(\n request: SuggestRequest,\n config_manager=Depends(get_config_manager),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"suggest_mappings_api\"):\n from ...services.mapping_service import MappingService\n service = MappingService(config_manager)\n try:\n return await service.get_suggestions(request.source_env_id, request.target_env_id)\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n# #endregion suggest_mappings_api\n\n# #endregion MappingsApi\n" }, { "contract_id": "MappingCreate", @@ -12930,7 +13779,7 @@ "contract_type": "DataClass", "file_path": "backend/src/api/routes/mappings.py", "start_line": 34, - "end_line": 47, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -12938,14 +13787,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion MappingResponse\n" + "body": "# #region MappingResponse [TYPE DataClass]\nclass MappingResponse(BaseModel):\n id: str\n source_env_id: str\n target_env_id: str\n source_db_uuid: str\n target_db_uuid: str\n source_db_name: str\n target_db_name: str\n engine: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion MappingResponse\n" }, { "contract_id": "SuggestRequest", "contract_type": "DataClass", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 49, - "end_line": 53, + "start_line": 48, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -12959,8 +13808,8 @@ "contract_id": "get_mappings", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 55, - "end_line": 73, + "start_line": 54, + "end_line": 72, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12978,8 +13827,8 @@ "contract_id": "create_mapping", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 75, - "end_line": 106, + "start_line": 74, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -12991,14 +13840,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE mapping is valid MappingCreate, db session is injected.\n# @POST DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.dict())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n" + "body": "# #region create_mapping [TYPE Function]\n# @BRIEF Create or update a database mapping.\n# @PRE mapping is valid MappingCreate, db session is injected.\n# @POST DatabaseMapping created or updated in database.\n@router.post(\"\", response_model=MappingResponse)\nasync def create_mapping(\n mapping: MappingCreate,\n db: Session = Depends(get_db),\n _ = Depends(has_permission(\"plugin:mapper\", \"EXECUTE\"))\n):\n with belief_scope(\"create_mapping\"):\n # Check if mapping already exists\n existing = db.query(DatabaseMapping).filter(\n DatabaseMapping.source_env_id == mapping.source_env_id,\n DatabaseMapping.target_env_id == mapping.target_env_id,\n DatabaseMapping.source_db_uuid == mapping.source_db_uuid\n ).first()\n\n if existing:\n existing.target_db_uuid = mapping.target_db_uuid\n existing.target_db_name = mapping.target_db_name\n existing.engine = mapping.engine\n db.commit()\n db.refresh(existing)\n return existing\n\n new_mapping = DatabaseMapping(**mapping.model_dump())\n db.add(new_mapping)\n db.commit()\n db.refresh(new_mapping)\n return new_mapping\n# #endregion create_mapping\n" }, { "contract_id": "suggest_mappings_api", "contract_type": "Function", "file_path": "backend/src/api/routes/mappings.py", - "start_line": 108, - "end_line": 125, + "start_line": 107, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -13081,7 +13930,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS fastapi, migration, api, sync, search, mapping]\n# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @RELATION DEPENDS_ON -> [DashboardMetadata]\n# @RELATION DEPENDS_ON -> [MigrationDryRunService]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n# @INVARIANT Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.\n# @PRE Backend core services initialized and Database session available.\n# @POST Migration tasks are enqueued or dry-run results are computed and returned.\n# @SIDE_EFFECT Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.\n# @DATA_CONTRACT [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]\n# @TEST_CONTRACT [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]\n# @TEST_SCENARIO [invalid_environment] -> [HTTP_400_or_404]\n# @TEST_SCENARIO [valid_execution] -> [success_payload_with_required_fields]\n# @TEST_EDGE [missing_field] ->[HTTP_400]\n# @TEST_EDGE [invalid_type] ->[validation_error]\n# @TEST_EDGE [external_fail] ->[HTTP_500]\n# @TEST_INVARIANT [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]\n\nfrom typing import Any, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.mapping_service import IdMappingService\nfrom ...core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...models.dashboard import DashboardMetadata, DashboardSelection\nfrom ...models.mapping import ResourceMapping\n\nlogger = cast(Any, logger)\n\nrouter = APIRouter(prefix=\"/api\", tags=[\"migration\"])\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI.\n# @PRE env_id is provided and exists in configured environments.\n# @POST Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.\n# @SIDE_EFFECT Reads environment configuration and performs remote Superset metadata retrieval over network.\n# @DATA_CONTRACT Input[str env_id] -> Output[List[DashboardMetadata]]\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dashboards_summary]\n@router.get(\"/environments/{env_id}/dashboards\", response_model=list[DashboardMetadata])\nasync def get_dashboards(\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"get_dashboards\", f\"env_id={env_id}\"):\n logger.reason(f\"Fetching dashboards for environment: {env_id}\")\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n\n if not env:\n logger.explore(f\"Environment {env_id} not found in configuration\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary()\n logger.reflect(f\"Retrieved {len(dashboards)} dashboards from {env_id}\")\n return dashboards\n\n\n# #endregion get_dashboards\n\n\n# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF 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: {e!s}\"\n )\n\n\n# #endregion execute_migration\n\n\n# #region dry_run_migration [C:5] [TYPE Function]\n# @BRIEF 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# #endregion dry_run_migration\n\n\n# #region get_migration_settings [C:3] [TYPE Function]\n# @BRIEF Read and return configured migration synchronization cron expression.\n# @PRE Configuration store is available and requester has READ permission.\n# @POST Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT Reads configuration from config manager.\n# @DATA_CONTRACT Input[None] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.get(\"/migration/settings\", response_model=dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# #endregion get_migration_settings\n\n\n# #region update_migration_settings [C:3] [TYPE Function]\n# @BRIEF Validate and persist migration synchronization cron expression update.\n# @PRE Payload includes \"cron\" key and requester has WRITE permission.\n# @POST Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.put(\"/migration/settings\", response_model=dict[str, str])\nasync def update_migration_settings(\n payload: dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# #endregion update_migration_settings\n\n\n# #region get_resource_mappings [C:3] [TYPE Function]\n# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: str | None = Query(None, description=\"Search by resource name or UUID\"),\n env_id: str | None = Query(None, description=\"Filter by environment ID\"),\n resource_type: str | None = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# #endregion get_resource_mappings\n\n\n# #region trigger_sync_now [C:3] [TYPE Function]\n# @BRIEF Trigger immediate ID synchronization for every configured environment.\n# @PRE At least one environment is configured and requester has EXECUTE permission.\n# @POST Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT Input[None] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION CALLS -> [sync_environment]\n@router.post(\"/migration/sync-now\", response_model=dict[str, Any])\nasync def trigger_sync_now(\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(\"trigger_sync_now\"):\n from ...core.logger import logger\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\",\n extra={\"src\": \"trigger_sync_now\"},\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\", extra={\"src\": \"trigger_sync_now\"})\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.error(f\"[trigger_sync_now][Error] Failed to sync {env.id}: {e}\")\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# #endregion trigger_sync_now\n\n# #endregion MigrationApi\n" + "body": "# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS fastapi, migration, api, sync, search, mapping]\n# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [AppDependencies]\n# @RELATION DEPENDS_ON -> [DatabaseModule]\n# @RELATION DEPENDS_ON -> [DashboardSelection]\n# @RELATION DEPENDS_ON -> [DashboardMetadata]\n# @RELATION DEPENDS_ON -> [MigrationDryRunService]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n# @INVARIANT Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.\n# @PRE Backend core services initialized and Database session available.\n# @POST Migration tasks are enqueued or dry-run results are computed and returned.\n# @SIDE_EFFECT Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.\n# @DATA_CONTRACT [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]\n# @TEST_CONTRACT [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]\n# @TEST_SCENARIO [invalid_environment] -> [HTTP_400_or_404]\n# @TEST_SCENARIO [valid_execution] -> [success_payload_with_required_fields]\n# @TEST_EDGE [missing_field] ->[HTTP_400]\n# @TEST_EDGE [invalid_type] ->[validation_error]\n# @TEST_EDGE [external_fail] ->[HTTP_500]\n# @TEST_INVARIANT [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]\n\nfrom typing import Any, cast\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom sqlalchemy.orm import Session\n\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.mapping_service import IdMappingService\nfrom ...core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, get_task_manager, has_permission\nfrom ...models.dashboard import DashboardMetadata, DashboardSelection\nfrom ...models.mapping import ResourceMapping\n\nlogger = cast(Any, logger)\n\nrouter = APIRouter(prefix=\"/api\", tags=[\"migration\"])\n\n\n# #region get_dashboards [C:3] [TYPE Function]\n# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI.\n# @PRE env_id is provided and exists in configured environments.\n# @POST Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.\n# @SIDE_EFFECT Reads environment configuration and performs remote Superset metadata retrieval over network.\n# @DATA_CONTRACT Input[str env_id] -> Output[List[DashboardMetadata]]\n# @RELATION CALLS -> [EXT:method:SupersetClient.get_dashboards_summary]\n@router.get(\"/environments/{env_id}/dashboards\", response_model=list[DashboardMetadata])\nasync def get_dashboards(\n env_id: str,\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"EXECUTE\")),\n):\n with belief_scope(\"get_dashboards\", f\"env_id={env_id}\"):\n logger.reason(f\"Fetching dashboards for environment: {env_id}\")\n environments = config_manager.get_environments()\n env = next((e for e in environments if e.id == env_id), None)\n\n if not env:\n logger.explore(f\"Environment {env_id} not found in configuration\")\n raise HTTPException(status_code=404, detail=\"Environment not found\")\n\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary()\n logger.reflect(f\"Retrieved {len(dashboards)} dashboards from {env_id}\")\n return dashboards\n\n\n# #endregion get_dashboards\n\n\n# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF 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.model_dump()\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: {e!s}\"\n )\n\n\n# #endregion execute_migration\n\n\n# #region dry_run_migration [C:5] [TYPE Function]\n# @BRIEF 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# #endregion dry_run_migration\n\n\n# #region get_migration_settings [C:3] [TYPE Function]\n# @BRIEF Read and return configured migration synchronization cron expression.\n# @PRE Configuration store is available and requester has READ permission.\n# @POST Returns {\"cron\": str} reflecting current persisted settings value.\n# @SIDE_EFFECT Reads configuration from config manager.\n# @DATA_CONTRACT Input[None] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.get(\"/migration/settings\", response_model=dict[str, str])\nasync def get_migration_settings(\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_migration_settings\"):\n config = config_manager.get_config()\n cron = config.settings.migration_sync_cron\n return {\"cron\": cron}\n\n\n# #endregion get_migration_settings\n\n\n# #region update_migration_settings [C:3] [TYPE Function]\n# @BRIEF Validate and persist migration synchronization cron expression update.\n# @PRE Payload includes \"cron\" key and requester has WRITE permission.\n# @POST Returns {\"cron\": str, \"status\": \"updated\"} and persists updated cron value.\n# @SIDE_EFFECT Mutates configuration and writes persisted config through config manager.\n# @DATA_CONTRACT Input[Dict[str, str]] -> Output[Dict[str, str]]\n# @RELATION DEPENDS_ON -> [AppDependencies]\n@router.put(\"/migration/settings\", response_model=dict[str, str])\nasync def update_migration_settings(\n payload: dict[str, str],\n config_manager=Depends(get_config_manager),\n _=Depends(has_permission(\"plugin:migration\", \"WRITE\")),\n):\n with belief_scope(\"update_migration_settings\"):\n if \"cron\" not in payload:\n raise HTTPException(\n status_code=400, detail=\"Missing 'cron' field in payload\"\n )\n\n cron_expr = payload[\"cron\"]\n\n config = config_manager.get_config()\n config.settings.migration_sync_cron = cron_expr\n config_manager.save_config(config)\n\n return {\"cron\": cron_expr, \"status\": \"updated\"}\n\n\n# #endregion update_migration_settings\n\n\n# #region get_resource_mappings [C:3] [TYPE Function]\n# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.\n# @PRE skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.\n# @POST Returns {\"items\": [...], \"total\": int} where items reflect applied filters and pagination.\n# @SIDE_EFFECT Executes database read queries against ResourceMapping table.\n# @DATA_CONTRACT Input[QueryParams] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [ResourceMapping]\n@router.get(\"/migration/mappings-data\", response_model=dict[str, Any])\nasync def get_resource_mappings(\n skip: int = Query(0, ge=0),\n limit: int = Query(50, ge=1, le=500),\n search: str | None = Query(None, description=\"Search by resource name or UUID\"),\n env_id: str | None = Query(None, description=\"Filter by environment ID\"),\n resource_type: str | None = Query(None, description=\"Filter by resource type\"),\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"plugin:migration\", \"READ\")),\n):\n with belief_scope(\"get_resource_mappings\"):\n query = db.query(ResourceMapping)\n\n if env_id:\n query = query.filter(ResourceMapping.environment_id == env_id)\n\n if resource_type:\n query = query.filter(ResourceMapping.resource_type == resource_type.upper())\n\n if search:\n search_term = f\"%{search}%\"\n query = query.filter(\n (ResourceMapping.resource_name.ilike(search_term))\n | (ResourceMapping.uuid.ilike(search_term))\n )\n\n total = query.count()\n mappings = (\n query.order_by(ResourceMapping.resource_type, ResourceMapping.resource_name)\n .offset(skip)\n .limit(limit)\n .all()\n )\n\n items = []\n for m in mappings:\n mapping = cast(Any, m)\n resource_type_value = (\n mapping.resource_type.value\n if mapping.resource_type is not None\n else None\n )\n last_synced_at = (\n mapping.last_synced_at.isoformat()\n if mapping.last_synced_at is not None\n else None\n )\n items.append(\n {\n \"id\": mapping.id,\n \"environment_id\": mapping.environment_id,\n \"resource_type\": resource_type_value,\n \"uuid\": mapping.uuid,\n \"remote_id\": mapping.remote_integer_id,\n \"resource_name\": mapping.resource_name,\n \"last_synced_at\": last_synced_at,\n }\n )\n\n return {\"items\": items, \"total\": total}\n\n\n# #endregion get_resource_mappings\n\n\n# #region trigger_sync_now [C:3] [TYPE Function]\n# @BRIEF Trigger immediate ID synchronization for every configured environment.\n# @PRE At least one environment is configured and requester has EXECUTE permission.\n# @POST Returns sync summary with synced/failed counts after attempting all environments.\n# @SIDE_EFFECT Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.\n# @DATA_CONTRACT Input[None] -> Output[Dict[str, Any]]\n# @RELATION DEPENDS_ON -> [IdMappingService]\n# @RELATION CALLS -> [sync_environment]\n@router.post(\"/migration/sync-now\", response_model=dict[str, Any])\nasync def trigger_sync_now(\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(\"trigger_sync_now\"):\n from ...core.logger import logger\n from ...models.mapping import Environment as EnvironmentModel\n\n config = config_manager.get_config()\n environments = config.environments\n\n if not environments:\n raise HTTPException(status_code=400, detail=\"No environments configured\")\n\n # Ensure each environment exists in DB (upsert) to satisfy FK constraints\n for env in environments:\n existing = db.query(EnvironmentModel).filter_by(id=env.id).first()\n if not existing:\n db_env = EnvironmentModel(\n id=env.id,\n name=env.name,\n url=env.url,\n credentials_id=env.id, # Use env.id as credentials reference\n )\n db.add(db_env)\n logger.reason(\n f\"Created environment row for {env.id}\",\n extra={\"src\": \"trigger_sync_now\"},\n )\n else:\n existing.name = env.name\n existing.url = env.url\n db.commit()\n\n service = IdMappingService(db)\n results = {\"synced\": [], \"failed\": []}\n\n for env in environments:\n try:\n client = SupersetClient(env)\n service.sync_environment(env.id, client)\n results[\"synced\"].append(env.id)\n logger.reason(f\"Synced environment {env.id}\", extra={\"src\": \"trigger_sync_now\"})\n except Exception as e:\n results[\"failed\"].append({\"env_id\": env.id, \"error\": str(e)})\n logger.error(f\"[trigger_sync_now][Error] Failed to sync {env.id}: {e}\")\n\n return {\n \"status\": \"completed\",\n \"synced_count\": len(results[\"synced\"]),\n \"failed_count\": len(results[\"failed\"]),\n \"details\": results,\n }\n\n\n# #endregion trigger_sync_now\n\n# #endregion MigrationApi\n" }, { "contract_id": "execute_migration", @@ -13117,7 +13966,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF 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: {e!s}\"\n )\n\n\n# #endregion execute_migration\n" + "body": "# #region execute_migration [C:5] [TYPE Function]\n# @BRIEF 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.model_dump()\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: {e!s}\"\n )\n\n\n# #endregion execute_migration\n" }, { "contract_id": "dry_run_migration", @@ -13677,7 +14526,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]\n#\n# @BRIEF Provides API endpoints for managing application settings and Superset environments.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_config_manager]\n# @RELATION DEPENDS_ON -> [has_permission]\n#\n# @PRE ConfigManager is initialized and accessible.\n# @POST Settings are read or written via ConfigManager.\n# @SIDE_EFFECT Persists config changes to disk via ConfigManager.\n# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse\n# @INVARIANT All settings changes must be persisted via ConfigManager.\n# @PUBLIC_API router\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.config import AppConfigRecord\nfrom ...models.llm import ValidationPolicy\nfrom ...models.storage import StorageConfig\nfrom ...models.translate import TranslationJob, TranslationSchedule\nfrom ...schemas.settings import (\n TranslationScheduleItem,\n ValidationPolicyCreate,\n ValidationPolicyResponse,\n ValidationPolicyUpdate,\n)\nfrom ...services.llm_prompt_templates import normalize_llm_settings\n\n\n# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response]\n# @BRIEF Response model for logging configuration with current task log level.\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# #endregion LoggingConfigResponse\n\nrouter = APIRouter()\n\n\n# #region _normalize_superset_env_url [C:1] [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# Auto-prepends https:// if no scheme is present.\n# @PRE raw_url can be empty.\n# @POST Returns normalized base URL with scheme.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[: -len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n\n\n# #endregion _normalize_superset_env_url\n\n\n# #region _validate_superset_connection_fast [C:2] [TYPE Function]\n# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.\n# @PRE env contains valid URL and credentials.\n# @POST Raises on auth/API failures; returns None on success.\ndef _validate_superset_connection_fast(env: Environment) -> None:\n client = SupersetClient(env)\n # 1) Explicit auth check\n client.authenticate()\n # 2) Single lightweight API call to ensure read access\n client.get_dashboards_page(\n query={\n \"page\": 0,\n \"page_size\": 1,\n \"columns\": [\"id\"],\n }\n )\n\n\n# #endregion _validate_superset_connection_fast\n\n\n# #region get_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves all application settings.\n# @PRE Config manager is available.\n# @POST Returns masked AppConfig.\n@router.get(\"\", response_model=AppConfig)\nasync def get_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_settings\"):\n logger.reason(\"Fetching all settings\", extra={\"src\": \"get_settings\"})\n config = config_manager.get_config().copy(deep=True)\n config.settings.llm = normalize_llm_settings(config.settings.llm)\n # Mask passwords\n for env in config.environments:\n if env.password:\n env.password = \"********\"\n return config\n\n\n# #endregion get_settings\n\n\n# #region get_features [C:1] [TYPE Function]\n# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.\n# @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins.\n# @PRE Config manager is available.\n# @POST Returns dict with dataset_review and health_monitor booleans.\n@router.get(\"/features\")\nasync def get_features(\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n return config_manager.get_config().settings.features.model_dump()\n\n\n# #endregion get_features\n\n\n# #region update_global_settings [C:2] [TYPE Function]\n# @BRIEF Updates global application settings.\n# @PRE New settings are provided.\n# @POST Global settings are updated.\n@router.patch(\"/global\", response_model=GlobalSettings)\nasync def update_global_settings(\n settings: GlobalSettings,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_global_settings\"):\n logger.reason(\"Updating global settings\", extra={\"src\": \"update_global_settings\"})\n\n config_manager.update_global_settings(settings)\n return settings\n\n\n# #endregion update_global_settings\n\n\n# #region get_storage_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves storage-specific settings.\n@router.get(\"/storage\", response_model=StorageConfig)\nasync def get_storage_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_storage_settings\"):\n return config_manager.get_config().settings.storage\n\n\n# #endregion get_storage_settings\n\n\n# #region update_storage_settings [C:2] [TYPE Function]\n# @BRIEF Updates storage-specific settings.\n# @POST Storage settings are updated and saved.\n@router.put(\"/storage\", response_model=StorageConfig)\nasync def update_storage_settings(\n storage: StorageConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_storage_settings\"):\n is_valid, message = config_manager.validate_path(storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n\n settings = config_manager.get_config().settings\n settings.storage = storage\n config_manager.update_global_settings(settings)\n return config_manager.get_config().settings.storage\n\n\n# #endregion update_storage_settings\n\n\n# #region get_environments [C:2] [TYPE Function]\n# @BRIEF Lists all configured Superset environments.\n# @PRE Config manager is available.\n# @POST Returns list of environments.\n@router.get(\"/environments\", response_model=list[Environment])\nasync def get_environments(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n logger.reason(\"Fetching environments\", extra={\"src\": \"get_environments\"})\n environments = config_manager.get_environments()\n return [\n env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n for env in environments\n ]\n\n\n# #endregion get_environments\n\n\n# #region add_environment [C:2] [TYPE Function]\n# @BRIEF Adds a new Superset environment.\n# @PRE Environment data is valid and reachable.\n# @POST Environment is added to config.\n@router.post(\"/environments\", response_model=Environment)\nasync def add_environment(\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"add_environment\"):\n logger.reason(f\"Adding environment {env.id}\", extra={\"src\": \"add_environment\"})\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # Validate connection before adding (fast path)\n try:\n _validate_superset_connection_fast(env)\n except Exception as e:\n logger.error(\n f\"[add_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n config_manager.add_environment(env)\n return env\n\n\n# #endregion add_environment\n\n\n# #region update_environment [C:2] [TYPE Function]\n# @BRIEF Updates an existing Superset environment.\n# @PRE ID and valid environment data are provided.\n# @POST Environment is updated in config.\n@router.put(\"/environments/{id}\", response_model=Environment)\nasync def update_environment(\n id: str,\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n with belief_scope(\"update_environment\"):\n logger.reason(f\"Updating environment {id}\", extra={\"src\": \"update_environment\"})\n\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # If password is masked, we need the real one for validation\n env_to_validate = env.copy(deep=True)\n if env_to_validate.password == \"********\":\n old_env = next(\n (e for e in config_manager.get_environments() if e.id == id), None\n )\n if old_env:\n env_to_validate.password = old_env.password\n\n # Validate connection before updating (fast path)\n try:\n _validate_superset_connection_fast(env_to_validate)\n except Exception as e:\n logger.error(\n f\"[update_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n if config_manager.update_environment(id, env):\n return env\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n\n# #endregion update_environment\n\n\n# #region delete_environment [C:2] [TYPE Function]\n# @BRIEF Deletes a Superset environment.\n# @PRE ID is provided.\n# @POST Environment is removed from config.\n@router.delete(\"/environments/{id}\")\nasync def delete_environment(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"delete_environment\"):\n logger.reason(f\"Deleting environment {id}\", extra={\"src\": \"delete_environment\"})\n config_manager.delete_environment(id)\n return {\"message\": f\"Environment {id} deleted\"}\n\n\n# #endregion delete_environment\n\n\n# #region test_environment_connection [C:2] [TYPE Function]\n# @BRIEF Tests the connection to a Superset environment.\n# @PRE ID is provided.\n# @POST Returns success or error status.\n@router.post(\"/environments/{id}/test\")\nasync def test_environment_connection(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"test_environment_connection\"):\n logger.reason(f\"Testing environment {id}\", extra={\"src\": \"test_environment_connection\"})\n\n # Find environment\n env = next((e for e in config_manager.get_environments() if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n try:\n _validate_superset_connection_fast(env)\n\n logger.info(\n f\"[test_environment_connection][Coherence:OK] Connection successful for {id}\"\n )\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n logger.error(\n f\"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}\"\n )\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# #endregion test_environment_connection\n\n\n# #region get_logging_config [C:2] [TYPE Function]\n# @BRIEF Retrieves current logging configuration.\n# @PRE Config manager is available.\n# @POST Returns logging configuration.\n@router.get(\"/logging\", response_model=LoggingConfigResponse)\nasync def get_logging_config(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_logging_config\"):\n logging_config = config_manager.get_config().settings.logging\n return LoggingConfigResponse(\n level=logging_config.level,\n task_log_level=logging_config.task_log_level,\n enable_belief_state=logging_config.enable_belief_state,\n )\n\n\n# #endregion get_logging_config\n\n\n# #region update_logging_config [C:2] [TYPE Function]\n# @BRIEF Updates logging configuration.\n# @PRE New logging config is provided.\n# @POST Logging configuration is updated and saved.\n@router.patch(\"/logging\", response_model=LoggingConfigResponse)\nasync def update_logging_config(\n config: LoggingConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_logging_config\"):\n logger.reason(\n f\"Updating logging config: level={config.level}, task_log_level={config.task_log_level}\", extra={\"src\": \"update_logging_config\"}\n )\n\n # Get current settings and update logging config\n settings = config_manager.get_config().settings\n settings.logging = config\n config_manager.update_global_settings(settings)\n\n return LoggingConfigResponse(\n level=config.level,\n task_log_level=config.task_log_level,\n enable_belief_state=config.enable_belief_state,\n )\n\n\n# #endregion update_logging_config\n\n\n# #region ConsolidatedSettingsResponse [C:1] [TYPE Class]\n# @BRIEF 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 app_timezone: str = \"Europe/Moscow\"\n\n\n# #endregion ConsolidatedSettingsResponse\n\n\n# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE Config manager is available and the caller holds admin settings read permission.\n# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": 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 \"is_multimodal\": bool(p.is_multimodal) if p.is_multimodal is not None else False,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.dict() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.dict(),\n storage=config.settings.storage.dict(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n app_timezone=config.settings.app_timezone,\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n\n\n# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE User has admin permissions, config is valid.\n# @POST Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n # Update app_timezone if provided\n if \"app_timezone\" in settings_patch:\n new_tz = settings_patch[\"app_timezone\"]\n from ...core.timezone import validate_timezone, invalidate_timezone_cache\n if not validate_timezone(new_tz):\n raise HTTPException(status_code=422, detail=f\"Invalid IANA timezone: {new_tz}\")\n current_settings.app_timezone = new_tz\n invalidate_timezone_cache()\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n\n\n# #region get_validation_policies [C:2] [TYPE Function]\n# @BRIEF Lists all validation policies.\n@router.get(\"/automation/policies\", response_model=list[ValidationPolicyResponse])\nasync def get_validation_policies(\n db: Session = Depends(get_db), _=Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_validation_policies\"):\n return db.query(ValidationPolicy).all()\n\n\n# #endregion get_validation_policies\n\n\n# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.dict())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n\n\n# #region update_validation_policy [C:2] [TYPE Function]\n# @BRIEF Updates an existing validation policy.\n@router.patch(\"/automation/policies/{id}\", response_model=ValidationPolicyResponse)\nasync def update_validation_policy(\n id: str,\n policy: ValidationPolicyUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n update_data = policy.dict(exclude_unset=True)\n for key, value in update_data.items():\n setattr(db_policy, key, value)\n\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion update_validation_policy\n\n\n# #region delete_validation_policy [C:2] [TYPE Function]\n# @BRIEF Deletes a validation policy.\n@router.delete(\"/automation/policies/{id}\")\nasync def delete_validation_policy(\n id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n db.delete(db_policy)\n db.commit()\n return {\"message\": \"Policy deleted\"}\n\n\n# #endregion delete_validation_policy\n\n\n# #region get_translation_schedules [C:2] [TYPE Function]\n# @BRIEF Lists all translation schedules joined with translation job names.\n@router.get(\"/automation/translation-schedules\", response_model=list[TranslationScheduleItem])\nasync def get_translation_schedules(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_translation_schedules\"):\n results = (\n db.query(\n TranslationSchedule,\n TranslationJob.name.label(\"job_name\"),\n )\n .outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id)\n .all()\n )\n return [\n TranslationScheduleItem(\n schedule_id=ts.id,\n job_id=ts.job_id,\n job_name=job_name,\n cron_expression=ts.cron_expression,\n timezone=ts.timezone,\n is_active=ts.is_active,\n last_run_at=ts.last_run_at,\n next_run_at=ts.next_run_at,\n created_by=ts.created_by,\n created_at=ts.created_at,\n )\n for ts, job_name in results\n ]\n\n\n# #endregion get_translation_schedules\n\n# #endregion SettingsRouter\n" + "body": "# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]\n#\n# @BRIEF Provides API endpoints for managing application settings and Superset environments.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [get_config_manager]\n# @RELATION DEPENDS_ON -> [has_permission]\n#\n# @PRE ConfigManager is initialized and accessible.\n# @POST Settings are read or written via ConfigManager.\n# @SIDE_EFFECT Persists config changes to disk via ConfigManager.\n# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse\n# @INVARIANT All settings changes must be persisted via ConfigManager.\n# @PUBLIC_API router\n\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom pydantic import BaseModel\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.config_models import AppConfig, Environment, GlobalSettings, LoggingConfig\nfrom ...core.database import get_db\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...dependencies import get_config_manager, has_permission\nfrom ...models.config import AppConfigRecord\nfrom ...models.llm import ValidationPolicy\nfrom ...models.storage import StorageConfig\nfrom ...models.translate import TranslationJob, TranslationSchedule\nfrom ...schemas.settings import (\n TranslationScheduleItem,\n ValidationPolicyCreate,\n ValidationPolicyResponse,\n ValidationPolicyUpdate,\n)\nfrom ...services.llm_prompt_templates import normalize_llm_settings\n\n\n# #region LoggingConfigResponse [C:1] [TYPE Class] [SEMANTICS logging, config, response]\n# @BRIEF Response model for logging configuration with current task log level.\nclass LoggingConfigResponse(BaseModel):\n level: str\n task_log_level: str\n enable_belief_state: bool\n\n\n# #endregion LoggingConfigResponse\n\nrouter = APIRouter()\n\n\n# #region _normalize_superset_env_url [C:1] [TYPE Function]\n# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.\n# Auto-prepends https:// if no scheme is present.\n# @PRE raw_url can be empty.\n# @POST Returns normalized base URL with scheme.\ndef _normalize_superset_env_url(raw_url: str) -> str:\n normalized = str(raw_url or \"\").strip().rstrip(\"/\")\n if normalized.lower().endswith(\"/api/v1\"):\n normalized = normalized[: -len(\"/api/v1\")]\n normalized = normalized.rstrip(\"/\")\n # Auto-prepend https:// if no scheme is present\n if normalized and not normalized.startswith(\"http://\") and not normalized.startswith(\"https://\"):\n normalized = f\"https://{normalized}\"\n return normalized\n\n\n# #endregion _normalize_superset_env_url\n\n\n# #region _validate_superset_connection_fast [C:2] [TYPE Function]\n# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.\n# @PRE env contains valid URL and credentials.\n# @POST Raises on auth/API failures; returns None on success.\ndef _validate_superset_connection_fast(env: Environment) -> None:\n client = SupersetClient(env)\n # 1) Explicit auth check\n client.authenticate()\n # 2) Single lightweight API call to ensure read access\n client.get_dashboards_page(\n query={\n \"page\": 0,\n \"page_size\": 1,\n \"columns\": [\"id\"],\n }\n )\n\n\n# #endregion _validate_superset_connection_fast\n\n\n# #region get_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves all application settings.\n# @PRE Config manager is available.\n# @POST Returns masked AppConfig.\n@router.get(\"\", response_model=AppConfig)\nasync def get_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_settings\"):\n logger.reason(\"Fetching all settings\", extra={\"src\": \"get_settings\"})\n config = config_manager.get_config().copy(deep=True)\n config.settings.llm = normalize_llm_settings(config.settings.llm)\n # Mask passwords\n for env in config.environments:\n if env.password:\n env.password = \"********\"\n return config\n\n\n# #endregion get_settings\n\n\n# #region get_features [C:1] [TYPE Function]\n# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.\n# @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins.\n# @PRE Config manager is available.\n# @POST Returns dict with dataset_review and health_monitor booleans.\n@router.get(\"/features\")\nasync def get_features(\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n return config_manager.get_config().settings.features.model_dump()\n\n\n# #endregion get_features\n\n\n# #region update_global_settings [C:2] [TYPE Function]\n# @BRIEF Updates global application settings.\n# @PRE New settings are provided.\n# @POST Global settings are updated.\n@router.patch(\"/global\", response_model=GlobalSettings)\nasync def update_global_settings(\n settings: GlobalSettings,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_global_settings\"):\n logger.reason(\"Updating global settings\", extra={\"src\": \"update_global_settings\"})\n\n config_manager.update_global_settings(settings)\n return settings\n\n\n# #endregion update_global_settings\n\n\n# #region get_storage_settings [C:2] [TYPE Function]\n# @BRIEF Retrieves storage-specific settings.\n@router.get(\"/storage\", response_model=StorageConfig)\nasync def get_storage_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_storage_settings\"):\n return config_manager.get_config().settings.storage\n\n\n# #endregion get_storage_settings\n\n\n# #region update_storage_settings [C:2] [TYPE Function]\n# @BRIEF Updates storage-specific settings.\n# @POST Storage settings are updated and saved.\n@router.put(\"/storage\", response_model=StorageConfig)\nasync def update_storage_settings(\n storage: StorageConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_storage_settings\"):\n is_valid, message = config_manager.validate_path(storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n\n settings = config_manager.get_config().settings\n settings.storage = storage\n config_manager.update_global_settings(settings)\n return config_manager.get_config().settings.storage\n\n\n# #endregion update_storage_settings\n\n\n# #region get_environments [C:2] [TYPE Function]\n# @BRIEF Lists all configured Superset environments.\n# @PRE Config manager is available.\n# @POST Returns list of environments.\n@router.get(\"/environments\", response_model=list[Environment])\nasync def get_environments(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_environments\"):\n logger.reason(\"Fetching environments\", extra={\"src\": \"get_environments\"})\n environments = config_manager.get_environments()\n return [\n env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n for env in environments\n ]\n\n\n# #endregion get_environments\n\n\n# #region add_environment [C:2] [TYPE Function]\n# @BRIEF Adds a new Superset environment.\n# @PRE Environment data is valid and reachable.\n# @POST Environment is added to config.\n@router.post(\"/environments\", response_model=Environment)\nasync def add_environment(\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"add_environment\"):\n logger.reason(f\"Adding environment {env.id}\", extra={\"src\": \"add_environment\"})\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # Validate connection before adding (fast path)\n try:\n _validate_superset_connection_fast(env)\n except Exception as e:\n logger.error(\n f\"[add_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n config_manager.add_environment(env)\n return env\n\n\n# #endregion add_environment\n\n\n# #region update_environment [C:2] [TYPE Function]\n# @BRIEF Updates an existing Superset environment.\n# @PRE ID and valid environment data are provided.\n# @POST Environment is updated in config.\n@router.put(\"/environments/{id}\", response_model=Environment)\nasync def update_environment(\n id: str,\n env: Environment,\n config_manager: ConfigManager = Depends(get_config_manager),\n):\n with belief_scope(\"update_environment\"):\n logger.reason(f\"Updating environment {id}\", extra={\"src\": \"update_environment\"})\n\n env = env.copy(update={\"url\": _normalize_superset_env_url(env.url)})\n\n # If password is masked, we need the real one for validation\n env_to_validate = env.copy(deep=True)\n if env_to_validate.password == \"********\":\n old_env = next(\n (e for e in config_manager.get_environments() if e.id == id), None\n )\n if old_env:\n env_to_validate.password = old_env.password\n\n # Validate connection before updating (fast path)\n try:\n _validate_superset_connection_fast(env_to_validate)\n except Exception as e:\n logger.error(\n f\"[update_environment][Coherence:Failed] Connection validation failed: {e}\"\n )\n raise HTTPException(\n status_code=400, detail=f\"Connection validation failed: {e}\"\n )\n\n if config_manager.update_environment(id, env):\n return env\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n\n# #endregion update_environment\n\n\n# #region delete_environment [C:2] [TYPE Function]\n# @BRIEF Deletes a Superset environment.\n# @PRE ID is provided.\n# @POST Environment is removed from config.\n@router.delete(\"/environments/{id}\")\nasync def delete_environment(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"delete_environment\"):\n logger.reason(f\"Deleting environment {id}\", extra={\"src\": \"delete_environment\"})\n config_manager.delete_environment(id)\n return {\"message\": f\"Environment {id} deleted\"}\n\n\n# #endregion delete_environment\n\n\n# #region test_environment_connection [C:2] [TYPE Function]\n# @BRIEF Tests the connection to a Superset environment.\n# @PRE ID is provided.\n# @POST Returns success or error status.\n@router.post(\"/environments/{id}/test\")\nasync def test_environment_connection(\n id: str, config_manager: ConfigManager = Depends(get_config_manager)\n):\n with belief_scope(\"test_environment_connection\"):\n logger.reason(f\"Testing environment {id}\", extra={\"src\": \"test_environment_connection\"})\n\n # Find environment\n env = next((e for e in config_manager.get_environments() if e.id == id), None)\n if not env:\n raise HTTPException(status_code=404, detail=f\"Environment {id} not found\")\n\n try:\n _validate_superset_connection_fast(env)\n\n logger.info(\n f\"[test_environment_connection][Coherence:OK] Connection successful for {id}\"\n )\n return {\"status\": \"success\", \"message\": \"Connection successful\"}\n except Exception as e:\n logger.error(\n f\"[test_environment_connection][Coherence:Failed] Connection failed for {id}: {e}\"\n )\n return {\"status\": \"error\", \"message\": str(e)}\n\n\n# #endregion test_environment_connection\n\n\n# #region get_logging_config [C:2] [TYPE Function]\n# @BRIEF Retrieves current logging configuration.\n# @PRE Config manager is available.\n# @POST Returns logging configuration.\n@router.get(\"/logging\", response_model=LoggingConfigResponse)\nasync def get_logging_config(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_logging_config\"):\n logging_config = config_manager.get_config().settings.logging\n return LoggingConfigResponse(\n level=logging_config.level,\n task_log_level=logging_config.task_log_level,\n enable_belief_state=logging_config.enable_belief_state,\n )\n\n\n# #endregion get_logging_config\n\n\n# #region update_logging_config [C:2] [TYPE Function]\n# @BRIEF Updates logging configuration.\n# @PRE New logging config is provided.\n# @POST Logging configuration is updated and saved.\n@router.patch(\"/logging\", response_model=LoggingConfigResponse)\nasync def update_logging_config(\n config: LoggingConfig,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_logging_config\"):\n logger.reason(\n f\"Updating logging config: level={config.level}, task_log_level={config.task_log_level}\", extra={\"src\": \"update_logging_config\"}\n )\n\n # Get current settings and update logging config\n settings = config_manager.get_config().settings\n settings.logging = config\n config_manager.update_global_settings(settings)\n\n return LoggingConfigResponse(\n level=config.level,\n task_log_level=config.task_log_level,\n enable_belief_state=config.enable_belief_state,\n )\n\n\n# #endregion update_logging_config\n\n\n# #region ConsolidatedSettingsResponse [C:1] [TYPE Class]\n# @BRIEF 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 app_timezone: str = \"Europe/Moscow\"\n\n\n# #endregion ConsolidatedSettingsResponse\n\n\n# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE Config manager is available and the caller holds admin settings read permission.\n# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": 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 \"is_multimodal\": bool(p.is_multimodal) if p.is_multimodal is not None else False,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.model_dump() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.model_dump(),\n storage=config.settings.storage.model_dump(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n app_timezone=config.settings.app_timezone,\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n\n\n# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE User has admin permissions, config is valid.\n# @POST Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n # Update app_timezone if provided\n if \"app_timezone\" in settings_patch:\n new_tz = settings_patch[\"app_timezone\"]\n from ...core.timezone import invalidate_timezone_cache, validate_timezone\n if not validate_timezone(new_tz):\n raise HTTPException(status_code=422, detail=f\"Invalid IANA timezone: {new_tz}\")\n current_settings.app_timezone = new_tz\n invalidate_timezone_cache()\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n\n\n# #region get_validation_policies [C:2] [TYPE Function]\n# @BRIEF Lists all validation policies.\n@router.get(\"/automation/policies\", response_model=list[ValidationPolicyResponse])\nasync def get_validation_policies(\n db: Session = Depends(get_db), _=Depends(has_permission(\"admin:settings\", \"READ\"))\n):\n with belief_scope(\"get_validation_policies\"):\n return db.query(ValidationPolicy).all()\n\n\n# #endregion get_validation_policies\n\n\n# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.model_dump())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n\n\n# #region update_validation_policy [C:2] [TYPE Function]\n# @BRIEF Updates an existing validation policy.\n@router.patch(\"/automation/policies/{id}\", response_model=ValidationPolicyResponse)\nasync def update_validation_policy(\n id: str,\n policy: ValidationPolicyUpdate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n update_data = policy.dict(exclude_unset=True)\n for key, value in update_data.items():\n setattr(db_policy, key, value)\n\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion update_validation_policy\n\n\n# #region delete_validation_policy [C:2] [TYPE Function]\n# @BRIEF Deletes a validation policy.\n@router.delete(\"/automation/policies/{id}\")\nasync def delete_validation_policy(\n id: str,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"delete_validation_policy\"):\n db_policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == id).first()\n if not db_policy:\n raise HTTPException(status_code=404, detail=\"Policy not found\")\n\n db.delete(db_policy)\n db.commit()\n return {\"message\": \"Policy deleted\"}\n\n\n# #endregion delete_validation_policy\n\n\n# #region get_translation_schedules [C:2] [TYPE Function]\n# @BRIEF Lists all translation schedules joined with translation job names.\n@router.get(\"/automation/translation-schedules\", response_model=list[TranslationScheduleItem])\nasync def get_translation_schedules(\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_translation_schedules\"):\n results = (\n db.query(\n TranslationSchedule,\n TranslationJob.name.label(\"job_name\"),\n )\n .outerjoin(TranslationJob, TranslationSchedule.job_id == TranslationJob.id)\n .all()\n )\n return [\n TranslationScheduleItem(\n schedule_id=ts.id,\n job_id=ts.job_id,\n job_name=job_name,\n cron_expression=ts.cron_expression,\n timezone=ts.timezone,\n is_active=ts.is_active,\n last_run_at=ts.last_run_at,\n next_run_at=ts.next_run_at,\n created_by=ts.created_by,\n created_at=ts.created_at,\n )\n for ts, job_name in results\n ]\n\n\n# #endregion get_translation_schedules\n\n# #endregion SettingsRouter\n" }, { "contract_id": "LoggingConfigResponse", @@ -13930,7 +14779,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE Config manager is available and the caller holds admin settings read permission.\n# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": 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 \"is_multimodal\": bool(p.is_multimodal) if p.is_multimodal is not None else False,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.dict() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.dict(),\n storage=config.settings.storage.dict(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n app_timezone=config.settings.app_timezone,\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n" + "body": "# #region get_consolidated_settings [C:4] [TYPE Function]\n# @BRIEF Retrieves all settings categories in a single call.\n# @PRE Config manager is available and the caller holds admin settings read permission.\n# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.\n# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.\n# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [has_permission]\n# @RELATION DEPENDS_ON -> [normalize_llm_settings]\n@router.get(\"/consolidated\", response_model=ConsolidatedSettingsResponse)\nasync def get_consolidated_settings(\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"READ\")),\n):\n with belief_scope(\"get_consolidated_settings\"):\n logger.reason(\"Fetching consolidated settings payload\")\n\n config = config_manager.get_config()\n\n from ...core.database import SessionLocal\n from ...services.llm_provider import LLMProviderService\n\n db = SessionLocal()\n notifications_payload = {}\n try:\n llm_service = LLMProviderService(db)\n providers = llm_service.get_all_providers()\n llm_providers_list = [\n {\n \"id\": p.id,\n \"provider_type\": 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 \"is_multimodal\": bool(p.is_multimodal) if p.is_multimodal is not None else False,\n }\n for p in providers\n ]\n\n config_record = (\n db.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n )\n if config_record and isinstance(config_record.payload, dict):\n notifications_payload = (\n config_record.payload.get(\"notifications\", {}) or {}\n )\n finally:\n db.close()\n\n normalized_llm = normalize_llm_settings(config.settings.llm)\n\n response_payload = ConsolidatedSettingsResponse(\n environments=[env.model_dump() for env in config.environments],\n connections=config.settings.connections,\n llm=normalized_llm,\n llm_providers=llm_providers_list,\n logging=config.settings.logging.model_dump(),\n storage=config.settings.storage.model_dump(),\n notifications=notifications_payload,\n features=config.settings.features.model_dump(),\n app_timezone=config.settings.app_timezone,\n )\n logger.reflect(\n \"Consolidated settings payload assembled\",\n extra={\n \"environment_count\": len(response_payload.environments),\n \"provider_count\": len(response_payload.llm_providers),\n },\n )\n return response_payload\n\n\n# #endregion get_consolidated_settings\n" }, { "contract_id": "update_consolidated_settings", @@ -13950,7 +14799,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE User has admin permissions, config is valid.\n# @POST Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n # Update app_timezone if provided\n if \"app_timezone\" in settings_patch:\n new_tz = settings_patch[\"app_timezone\"]\n from ...core.timezone import validate_timezone, invalidate_timezone_cache\n if not validate_timezone(new_tz):\n raise HTTPException(status_code=422, detail=f\"Invalid IANA timezone: {new_tz}\")\n current_settings.app_timezone = new_tz\n invalidate_timezone_cache()\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n" + "body": "# #region update_consolidated_settings [C:2] [TYPE Function]\n# @BRIEF Bulk update application settings from the consolidated view.\n# @PRE User has admin permissions, config is valid.\n# @POST Settings are updated and saved via ConfigManager.\n@router.patch(\"/consolidated\")\nasync def update_consolidated_settings(\n settings_patch: dict,\n config_manager: ConfigManager = Depends(get_config_manager),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"update_consolidated_settings\"):\n logger.reason(\n \"Applying consolidated settings patch\", extra={\"src\": \"update_consolidated_settings\"}\n )\n\n current_config = config_manager.get_config()\n current_settings = current_config.settings\n\n # Update connections if provided\n if \"connections\" in settings_patch:\n current_settings.connections = settings_patch[\"connections\"]\n\n # Update LLM if provided\n if \"llm\" in settings_patch:\n current_settings.llm = normalize_llm_settings(settings_patch[\"llm\"])\n\n # Update Logging if provided\n if \"logging\" in settings_patch:\n current_settings.logging = LoggingConfig(**settings_patch[\"logging\"])\n\n # Update Storage if provided\n if \"storage\" in settings_patch:\n new_storage = StorageConfig(**settings_patch[\"storage\"])\n is_valid, message = config_manager.validate_path(new_storage.root_path)\n if not is_valid:\n raise HTTPException(status_code=400, detail=message)\n current_settings.storage = new_storage\n\n # Update Features if provided\n if \"features\" in settings_patch:\n from ...core.config_models import FeaturesConfig\n\n current_settings.features = FeaturesConfig(**settings_patch[\"features\"])\n\n if \"notifications\" in settings_patch:\n payload = config_manager.get_payload()\n payload[\"notifications\"] = settings_patch[\"notifications\"]\n config_manager.save_config(payload)\n\n # Update app_timezone if provided\n if \"app_timezone\" in settings_patch:\n new_tz = settings_patch[\"app_timezone\"]\n from ...core.timezone import invalidate_timezone_cache, validate_timezone\n if not validate_timezone(new_tz):\n raise HTTPException(status_code=422, detail=f\"Invalid IANA timezone: {new_tz}\")\n current_settings.app_timezone = new_tz\n invalidate_timezone_cache()\n\n config_manager.update_global_settings(current_settings)\n return {\"status\": \"success\", \"message\": \"Settings updated\"}\n\n\n# #endregion update_consolidated_settings\n" }, { "contract_id": "get_validation_policies", @@ -13986,7 +14835,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.dict())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n" + "body": "# #region create_validation_policy [C:2] [TYPE Function]\n# @BRIEF Creates a new validation policy.\n@router.post(\"/automation/policies\", response_model=ValidationPolicyResponse)\nasync def create_validation_policy(\n policy: ValidationPolicyCreate,\n db: Session = Depends(get_db),\n _=Depends(has_permission(\"admin:settings\", \"WRITE\")),\n):\n with belief_scope(\"create_validation_policy\"):\n db_policy = ValidationPolicy(**policy.model_dump())\n db.add(db_policy)\n db.commit()\n db.refresh(db_policy)\n return db_policy\n\n\n# #endregion create_validation_policy\n" }, { "contract_id": "update_validation_policy", @@ -14707,7 +15556,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslateJobRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Translation Job CRUD and datasource column routes.\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope, logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.service import (\n TranslateJobService,\n get_datasource_columns as fetch_datasource_columns_service,\n job_to_response,\n)\nfrom ....plugins.translate.service_target_schema import validate_target_table_schema\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n DatasourceColumnsResponse,\n DuplicateJobResponse,\n TargetSchemaValidationRequest,\n TargetSchemaValidationResponse,\n TranslateJobCreate,\n TranslateJobResponse,\n TranslateJobUpdate,\n)\nfrom ._router import router\n\n# ============================================================\n# Translation Job CRUD\n# ============================================================\n\n# #region list_jobs [C:4] [TYPE Function]\n# @BRIEF List all translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of translation jobs.\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: str | None = 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 logger.reason(f\"list_jobs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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:4] [TYPE Function]\n# @BRIEF Get a single translation job by ID.\n# @PRE User has translate.job.view permission.\n# @POST Returns the translation job.\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 logger.reason(f\"get_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission.\n# @POST Returns the created translation job.\n# @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.\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 logger.reason(f\"create_job — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission.\n# @POST Returns the updated translation job.\n# @SIDE_EFFECT Re-detects database_dialect if datasource changed.\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 logger.reason(f\"update_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission.\n# @POST Job is deleted.\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 logger.reason(f\"delete_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission.\n# @POST Returns the duplicated job with status DRAFT.\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 logger.reason(f\"duplicate_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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 get_datasource_columns [C:4] [TYPE Function]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RATIONALE Dialect detection from Superset connection cached on TranslationJob at save time.\n# @REJECTED Hardcoding dialect list per database — would drift from actual Superset config.\n# @COMPLEXITY 4\n\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: str | None = 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 logger.explore(\"list_datasources failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n\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 logger.reason(\n f\"get_datasource_columns — Datasource: {datasource_id}, Env: {env_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"}\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 logger.explore(\"get_datasource_columns failed\", extra={\"src\": \"translate_routes\", \"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\n# #region check_target_schema [C:3] [TYPE Function] [SEMANTICS api,translate,validate]\n# @BRIEF Проверяет схему целевой таблицы: какие колонки ожидаются, какие есть, каких не хватает.\n# @RELATION DEPENDS_ON -> [validate_target_table_schema]\n# @RELATION DEPENDS_ON -> [TargetSchemaValidationRequest]\n# @RELATION DEPENDS_ON -> [TargetSchemaValidationResponse]\n# @RATIONALE Позволяет UI показать пользователю подсказку о недостающих колонках\n# на этапе настройки джобы, до запуска перевода.\n@router.post(\"/check-target-schema\", response_model=TargetSchemaValidationResponse)\nasync def check_target_schema(\n payload: TargetSchemaValidationRequest,\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 \"\"\"Check target table schema — validate expected vs actual columns for translation inserts.\"\"\"\n logger.reason(\n f\"check_target_schema — Table: {payload.target_schema}.{payload.target_table}, \"\n f\"Env: {payload.environment_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"},\n )\n try:\n result = validate_target_table_schema(payload, config_manager)\n return result\n except Exception as e:\n logger.explore(\"check_target_schema failed\",\n extra={\"src\": \"translate_routes\", \"error\": str(e)})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=str(e),\n all_present=False,\n )\n# #endregion check_target_schema\n\n\n# #endregion TranslateJobRoutesModule\n" + "body": "# #region TranslateJobRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]\n# @BRIEF Translation Job CRUD and datasource column routes.\n# @LAYER API\n\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ....core.config_manager import ConfigManager\nfrom ....core.database import get_db\nfrom ....core.logger import belief_scope, logger\nfrom ....dependencies import get_config_manager, get_current_user, has_permission\nfrom ....plugins.translate.service import (\n TranslateJobService,\n get_datasource_columns as fetch_datasource_columns_service,\n)\nfrom ....plugins.translate.service_target_schema import validate_target_table_schema\nfrom ....plugins.translate.service_utils import job_to_response\nfrom ....schemas.auth import User\nfrom ....schemas.translate import (\n DatasourceColumnsResponse,\n DuplicateJobResponse,\n TargetSchemaValidationRequest,\n TargetSchemaValidationResponse,\n TranslateJobCreate,\n TranslateJobResponse,\n TranslateJobUpdate,\n)\nfrom ._router import router\n\n# ============================================================\n# Translation Job CRUD\n# ============================================================\n\n# #region list_jobs [C:4] [TYPE Function]\n# @BRIEF List all translation jobs.\n# @PRE User has translate.job.view permission.\n# @POST Returns list of translation jobs.\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: str | None = 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 logger.reason(f\"list_jobs — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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:4] [TYPE Function]\n# @BRIEF Get a single translation job by ID.\n# @PRE User has translate.job.view permission.\n# @POST Returns the translation job.\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 logger.reason(f\"get_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Create a new translation job.\n# @PRE User has translate.job.create permission.\n# @POST Returns the created translation job.\n# @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.\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 logger.reason(f\"create_job — User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Update an existing translation job.\n# @PRE User has translate.job.edit permission.\n# @POST Returns the updated translation job.\n# @SIDE_EFFECT Re-detects database_dialect if datasource changed.\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 logger.reason(f\"update_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Delete a translation job.\n# @PRE User has translate.job.delete permission.\n# @POST Job is deleted.\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 logger.reason(f\"delete_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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]\n# @BRIEF Duplicate a translation job.\n# @PRE User has translate.job.create permission.\n# @POST Returns the duplicated job with status DRAFT.\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 logger.reason(f\"duplicate_job — Job: {job_id}, User: {current_user.username}\", extra={\"src\": \"translate_routes\"})\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 get_datasource_columns [C:4] [TYPE Function]\n# @BRIEF Get column metadata and database dialect for a Superset datasource.\n# @PRE User has translate.job.view permission.\n# @POST Returns column list with metadata and database_dialect.\n# @SIDE_EFFECT Queries Superset API for dataset detail and database info.\n# @RATIONALE Dialect detection from Superset connection cached on TranslationJob at save time.\n# @REJECTED Hardcoding dialect list per database — would drift from actual Superset config.\n# @COMPLEXITY 4\n\n@router.get(\"/datasources\")\nasync def list_datasources(\n env_id: str = Query(..., description=\"Superset environment ID\"),\n search: str | None = 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 logger.explore(\"list_datasources failed\", extra={\"src\": \"translate_routes\", \"error\": str(e)})\n raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))\n\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 logger.reason(\n f\"get_datasource_columns — Datasource: {datasource_id}, Env: {env_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"}\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 logger.explore(\"get_datasource_columns failed\", extra={\"src\": \"translate_routes\", \"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\n# #region check_target_schema [C:3] [TYPE Function] [SEMANTICS api,translate,validate]\n# @BRIEF Проверяет схему целевой таблицы: какие колонки ожидаются, какие есть, каких не хватает.\n# @RELATION DEPENDS_ON -> [validate_target_table_schema]\n# @RELATION DEPENDS_ON -> [TargetSchemaValidationRequest]\n# @RELATION DEPENDS_ON -> [TargetSchemaValidationResponse]\n# @RATIONALE Позволяет UI показать пользователю подсказку о недостающих колонках\n# на этапе настройки джобы, до запуска перевода.\n@router.post(\"/check-target-schema\", response_model=TargetSchemaValidationResponse)\nasync def check_target_schema(\n payload: TargetSchemaValidationRequest,\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 \"\"\"Check target table schema — validate expected vs actual columns for translation inserts.\"\"\"\n logger.reason(\n f\"check_target_schema — Table: {payload.target_schema}.{payload.target_table}, \"\n f\"Env: {payload.environment_id}, User: {current_user.username}\",\n extra={\"src\": \"translate_routes\"},\n )\n try:\n result = validate_target_table_schema(payload, config_manager)\n return result\n except Exception as e:\n logger.explore(\"check_target_schema failed\",\n extra={\"src\": \"translate_routes\", \"error\": str(e)})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=str(e),\n all_present=False,\n )\n# #endregion check_target_schema\n\n\n# #endregion TranslateJobRoutesModule\n" }, { "contract_id": "list_jobs", @@ -15530,7 +16379,7 @@ "contract_type": "Module", "file_path": "backend/src/api/routes/validation.py", "start_line": 1, - "end_line": 280, + "end_line": 279, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -15561,7 +16410,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, validation, api, task, run]\n# @BRIEF API routes for validation task management and run history.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ValidationTaskService]\n# @RELATION DEPENDS_ON -> [ValidationRunService]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom datetime import date\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.database import get_db\nfrom ...core.logger import logger\nfrom ...core.scheduler import SchedulerService\nfrom ...core.task_manager import TaskManager\nfrom ...dependencies import get_config_manager, get_current_user, get_scheduler_service, get_task_manager, has_permission\nfrom ...schemas.auth import User\nfrom ...schemas.validation import (\n TriggerRunResponse,\n ValidationRunDetailResponse,\n ValidationRunListResponse,\n ValidationTaskCreate,\n ValidationTaskListResponse,\n ValidationTaskResponse,\n ValidationTaskUpdate,\n)\nfrom ...services.validation_run_service import ValidationRunService\nfrom ...services.validation_service import ValidationTaskService\n\n# #region router [C:1] [TYPE Global]\nrouter = APIRouter(tags=[\"Validation\"])\n# #endregion router\n\n\n# #region _get_task_service [C:1] [TYPE Function]\ndef _get_task_service(\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n) -> ValidationTaskService:\n return ValidationTaskService(db=db, config_manager=config_manager, username=current_user.username)\n# #endregion _get_task_service\n\n\n# #region _get_run_service [C:1] [TYPE Function]\ndef _get_run_service(\n db: Session = Depends(get_db),\n) -> ValidationRunService:\n return ValidationRunService(db=db)\n# #endregion _get_run_service\n\n\n# ============================================================\n# Tasks CRUD\n# ============================================================\n\n\n# #region list_tasks [C:4] [TYPE Function]\n# @RATIONALE Route handler delegates to service with permission check — keeps API layer thin and testable; auth gate happens before any business logic.\n# @REJECTED Business logic in route handler rejected — would duplicate validation logic and make permission checks harder to audit.\n@router.get(\"/tasks\", response_model=ValidationTaskListResponse)\nasync def list_tasks(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n is_active: bool | None = Query(None),\n environment_id: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"VIEW\")),\n service: ValidationTaskService = Depends(_get_task_service),\n):\n \"\"\"List all validation tasks with pagination and optional filters.\"\"\"\n logger.reason(f\"list_tasks — User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n total, items = service.list_tasks(is_active=is_active, environment_id=environment_id, page=page, page_size=page_size)\n return ValidationTaskListResponse(items=items, total=total, page=page, page_size=page_size)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_tasks\n\n\n# #region create_task [C:4] [TYPE Function]\n# @RATIONALE Returns 201 with created resource — follows REST conventions for POST resource creation; signals to clients that a new entity was created.\n# @REJECTED Returning 200 with wrapped response rejected — non-standard for POST create; 201 Created signals the correct semantics to HTTP clients and code generators.\n@router.post(\"/tasks\", response_model=ValidationTaskResponse, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n payload: ValidationTaskCreate,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"CREATE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Create a new validation task with provider and environment validation.\"\"\"\n logger.reason(f\"create_task — User: {current_user.username}, name: {payload.name}\", extra={\"src\": \"validation_routes\"})\n try:\n result = service.create_task(payload)\n scheduler.reload_validation_policy(result.id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_task\n\n\n# #region get_task [C:4] [TYPE Function]\n# @RATIONALE Joins task detail with recent runs in a single endpoint — reduces frontend API calls from 2 to 1, improving page load performance.\n# @REJECTED Separate /tasks/{id}/runs endpoint for recent runs rejected — would require frontend to make 2 sequential API calls, doubling load time on the config page.\n@router.get(\"/tasks/{task_id}\", response_model=dict)\nasync def get_task(\n task_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"VIEW\")),\n task_service: ValidationTaskService = Depends(_get_task_service),\n run_service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Get a validation task with its recent runs.\"\"\"\n logger.reason(f\"get_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n task = task_service.get_task(task_id)\n from ...schemas.validation import ValidationRunResponse\n # Find recent runs for this specific task via task_id\n recent = run_service.list_runs(\n task_id=task_id,\n dashboard_id=task.dashboard_ids[0] if task.dashboard_ids else None,\n environment_id=task.environment_id,\n page=1,\n page_size=10,\n )\n return {\n \"task\": task.model_dump(),\n \"recent_runs\": [r.model_dump() for r in recent[1]],\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_task\n\n\n# #region update_task [C:4] [TYPE Function]\n# @RATIONALE PUT semantics for full resource update — consistent with existing API patterns; Pydantic's exclude_unset handles partial updates while keeping PUT as the endpoint verb.\n# @REJECTED PATCH semantics rejected — using PUT with exclude_unset provides the same partial-update flexibility without introducing a second HTTP method for the same resource.\n@router.put(\"/tasks/{task_id}\", response_model=ValidationTaskResponse)\nasync def update_task(\n task_id: str,\n payload: ValidationTaskUpdate,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"EDIT\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Update a validation task.\"\"\"\n logger.reason(f\"update_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n result = service.update_task(task_id, payload)\n scheduler.reload_validation_policy(result.id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_task\n\n\n# #region delete_task [C:4] [TYPE Function]\n# @RATIONALE DELETE returns 204 No Content per REST convention — minimizes response payload for delete operations; supports optional delete_runs query param for cascade control.\n# @REJECTED Returning deleted resource in response body rejected — unnecessary data transfer; 204 with no body is the REST standard for delete operations.\n@router.delete(\"/tasks/{task_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_task(\n task_id: str,\n delete_runs: bool = Query(False, description=\"Also delete associated validation runs\"),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"DELETE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Delete a validation task.\"\"\"\n logger.reason(f\"delete_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n service.delete_task(task_id, delete_runs=delete_runs)\n scheduler.remove_validation_job(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_task\n\n\n# ============================================================\n# Run — trigger immediate validation\n# ============================================================\n\n\n# #region trigger_run [C:4] [TYPE Function]\n# @RATIONALE Returns spawned task ID so frontend can poll for completion — enables progress tracking without WebSocket. Async spawn is essential since LLM validation can take 30+ seconds.\n# @REJECTED Blocking until run completes rejected — LLM-based validation is slow (30-120s); synchronous wait would timeout HTTP requests and create terrible UX.\n@router.post(\"/tasks/{task_id}/run\", response_model=TriggerRunResponse)\nasync def trigger_run(\n task_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"EXECUTE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n \"\"\"Trigger immediate validation for a task.\"\"\"\n logger.reason(f\"trigger_run — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n spawned_task_id = await service.trigger_run(task_id, task_manager)\n return TriggerRunResponse(task_id=task_id, spawned_task_id=spawned_task_id, status=\"PENDING\", message=\"Validation task spawned\")\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion trigger_run\n\n\n# ============================================================\n# Runs — history listing and detail\n# ============================================================\n\n\n# #region list_runs [C:4] [TYPE Function]\n# @RATIONALE Exposes 6 query parameters for cross-task run filtering — matches the run service's filter capabilities exactly, enabling flexible history exploration.\n# @REJECTED Fixed filter set rejected — would limit the history page's debugging use cases; users need to filter by task, status, dashboard, environment, and date range.\n@router.get(\"/runs\", response_model=ValidationRunListResponse)\nasync def list_runs(\n task_id: str | None = Query(None, description=\"Filter by spawned task ID\"),\n dashboard_id: str | None = Query(None, description=\"Filter by dashboard ID\"),\n environment_id: str | None = Query(None, description=\"Filter by environment ID\"),\n status: str | None = Query(None, description=\"Filter by status: PASS, WARN, FAIL, UNKNOWN\"),\n date_from: date | None = Query(None, description=\"Start date (ISO format)\"),\n date_to: date | None = Query(None, description=\"End date (ISO format)\"),\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(\"validation.run\", \"VIEW\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"List validation runs with cross-task filtering and pagination.\"\"\"\n logger.reason(f\"list_runs — User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n total, items = service.list_runs(\n task_id=task_id, dashboard_id=dashboard_id, environment_id=environment_id,\n status=status, date_from=date_from, date_to=date_to, page=page, page_size=page_size,\n )\n return ValidationRunListResponse(items=items, total=total, page=page, page_size=page_size)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n\n\n# #region get_run_detail [C:4] [TYPE Function]\n# @RATIONALE Returns full run detail including issues list and raw response for the report view — single endpoint serves the complete detail page without multiple fetches.\n# @REJECTED Paginated issues endpoint rejected — issues per run are typically < 20 items; pagination overhead is unnecessary for such small collections.\n@router.get(\"/runs/{run_id}\", response_model=ValidationRunDetailResponse)\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.run\", \"VIEW\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Get detailed validation run info including issues and raw response.\"\"\"\n logger.reason(f\"get_run_detail — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n return service.get_run_detail(run_id)\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# #region delete_run [C:3] [TYPE Function]\n# @RATIONALE Returns 404 when run not found — explicit error handling rather than silent success; helps frontend distinguish between successful delete and missing resource.\n# @REJECTED Silent success on missing run rejected — would mask bugs in the UI that reference already-deleted runs; explicit 404 helps debugging.\n@router.delete(\"/runs/{run_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.run\", \"DELETE\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Delete a validation run record.\"\"\"\n logger.reason(f\"delete_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n if not service.delete_run(run_id):\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Run not found\")\n# #endregion delete_run\n\n\n# #endregion ValidationRoutes\n" + "body": "# #region ValidationRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, validation, api, task, run]\n# @BRIEF API routes for validation task management and run history.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ValidationTaskService]\n# @RELATION DEPENDS_ON -> [ValidationRunService]\n# @RELATION DEPENDS_ON -> [TaskManager]\n\nfrom datetime import date\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query, status\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.database import get_db\nfrom ...core.logger import logger\nfrom ...core.scheduler import SchedulerService\nfrom ...core.task_manager import TaskManager\nfrom ...dependencies import get_config_manager, get_current_user, get_scheduler_service, get_task_manager, has_permission\nfrom ...schemas.auth import User\nfrom ...schemas.validation import (\n TriggerRunResponse,\n ValidationRunDetailResponse,\n ValidationRunListResponse,\n ValidationTaskCreate,\n ValidationTaskListResponse,\n ValidationTaskResponse,\n ValidationTaskUpdate,\n)\nfrom ...services.validation_run_service import ValidationRunService\nfrom ...services.validation_service import ValidationTaskService\n\n# #region router [C:1] [TYPE Global]\nrouter = APIRouter(tags=[\"Validation\"])\n# #endregion router\n\n\n# #region _get_task_service [C:1] [TYPE Function]\ndef _get_task_service(\n db: Session = Depends(get_db),\n config_manager: ConfigManager = Depends(get_config_manager),\n current_user: User = Depends(get_current_user),\n) -> ValidationTaskService:\n return ValidationTaskService(db=db, config_manager=config_manager, username=current_user.username)\n# #endregion _get_task_service\n\n\n# #region _get_run_service [C:1] [TYPE Function]\ndef _get_run_service(\n db: Session = Depends(get_db),\n) -> ValidationRunService:\n return ValidationRunService(db=db)\n# #endregion _get_run_service\n\n\n# ============================================================\n# Tasks CRUD\n# ============================================================\n\n\n# #region list_tasks [C:4] [TYPE Function]\n# @RATIONALE Route handler delegates to service with permission check — keeps API layer thin and testable; auth gate happens before any business logic.\n# @REJECTED Business logic in route handler rejected — would duplicate validation logic and make permission checks harder to audit.\n@router.get(\"/tasks\", response_model=ValidationTaskListResponse)\nasync def list_tasks(\n page: int = Query(1, ge=1),\n page_size: int = Query(20, ge=1, le=100),\n is_active: bool | None = Query(None),\n environment_id: str | None = Query(None),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"VIEW\")),\n service: ValidationTaskService = Depends(_get_task_service),\n):\n \"\"\"List all validation tasks with pagination and optional filters.\"\"\"\n logger.reason(f\"list_tasks — User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n total, items = service.list_tasks(is_active=is_active, environment_id=environment_id, page=page, page_size=page_size)\n return ValidationTaskListResponse(items=items, total=total, page=page, page_size=page_size)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_tasks\n\n\n# #region create_task [C:4] [TYPE Function]\n# @RATIONALE Returns 201 with created resource — follows REST conventions for POST resource creation; signals to clients that a new entity was created.\n# @REJECTED Returning 200 with wrapped response rejected — non-standard for POST create; 201 Created signals the correct semantics to HTTP clients and code generators.\n@router.post(\"/tasks\", response_model=ValidationTaskResponse, status_code=status.HTTP_201_CREATED)\nasync def create_task(\n payload: ValidationTaskCreate,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"CREATE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Create a new validation task with provider and environment validation.\"\"\"\n logger.reason(f\"create_task — User: {current_user.username}, name: {payload.name}\", extra={\"src\": \"validation_routes\"})\n try:\n result = service.create_task(payload)\n scheduler.reload_validation_policy(result.id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion create_task\n\n\n# #region get_task [C:4] [TYPE Function]\n# @RATIONALE Joins task detail with recent runs in a single endpoint — reduces frontend API calls from 2 to 1, improving page load performance.\n# @REJECTED Separate /tasks/{id}/runs endpoint for recent runs rejected — would require frontend to make 2 sequential API calls, doubling load time on the config page.\n@router.get(\"/tasks/{task_id}\", response_model=dict)\nasync def get_task(\n task_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"VIEW\")),\n task_service: ValidationTaskService = Depends(_get_task_service),\n run_service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Get a validation task with its recent runs.\"\"\"\n logger.reason(f\"get_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n task = task_service.get_task(task_id)\n # Find recent runs for this specific task via task_id\n recent = run_service.list_runs(\n task_id=task_id,\n dashboard_id=task.dashboard_ids[0] if task.dashboard_ids else None,\n environment_id=task.environment_id,\n page=1,\n page_size=10,\n )\n return {\n \"task\": task.model_dump(),\n \"recent_runs\": [r.model_dump() for r in recent[1]],\n }\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion get_task\n\n\n# #region update_task [C:4] [TYPE Function]\n# @RATIONALE PUT semantics for full resource update — consistent with existing API patterns; Pydantic's exclude_unset handles partial updates while keeping PUT as the endpoint verb.\n# @REJECTED PATCH semantics rejected — using PUT with exclude_unset provides the same partial-update flexibility without introducing a second HTTP method for the same resource.\n@router.put(\"/tasks/{task_id}\", response_model=ValidationTaskResponse)\nasync def update_task(\n task_id: str,\n payload: ValidationTaskUpdate,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"EDIT\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Update a validation task.\"\"\"\n logger.reason(f\"update_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n result = service.update_task(task_id, payload)\n scheduler.reload_validation_policy(result.id)\n return result\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion update_task\n\n\n# #region delete_task [C:4] [TYPE Function]\n# @RATIONALE DELETE returns 204 No Content per REST convention — minimizes response payload for delete operations; supports optional delete_runs query param for cascade control.\n# @REJECTED Returning deleted resource in response body rejected — unnecessary data transfer; 204 with no body is the REST standard for delete operations.\n@router.delete(\"/tasks/{task_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_task(\n task_id: str,\n delete_runs: bool = Query(False, description=\"Also delete associated validation runs\"),\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"DELETE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n scheduler: SchedulerService = Depends(get_scheduler_service),\n):\n \"\"\"Delete a validation task.\"\"\"\n logger.reason(f\"delete_task — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n service.delete_task(task_id, delete_runs=delete_runs)\n scheduler.remove_validation_job(task_id)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))\n# #endregion delete_task\n\n\n# ============================================================\n# Run — trigger immediate validation\n# ============================================================\n\n\n# #region trigger_run [C:4] [TYPE Function]\n# @RATIONALE Returns spawned task ID so frontend can poll for completion — enables progress tracking without WebSocket. Async spawn is essential since LLM validation can take 30+ seconds.\n# @REJECTED Blocking until run completes rejected — LLM-based validation is slow (30-120s); synchronous wait would timeout HTTP requests and create terrible UX.\n@router.post(\"/tasks/{task_id}/run\", response_model=TriggerRunResponse)\nasync def trigger_run(\n task_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.task\", \"EXECUTE\")),\n service: ValidationTaskService = Depends(_get_task_service),\n task_manager: TaskManager = Depends(get_task_manager),\n):\n \"\"\"Trigger immediate validation for a task.\"\"\"\n logger.reason(f\"trigger_run — Task: {task_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n spawned_task_id = await service.trigger_run(task_id, task_manager)\n return TriggerRunResponse(task_id=task_id, spawned_task_id=spawned_task_id, status=\"PENDING\", message=\"Validation task spawned\")\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))\n# #endregion trigger_run\n\n\n# ============================================================\n# Runs — history listing and detail\n# ============================================================\n\n\n# #region list_runs [C:4] [TYPE Function]\n# @RATIONALE Exposes 6 query parameters for cross-task run filtering — matches the run service's filter capabilities exactly, enabling flexible history exploration.\n# @REJECTED Fixed filter set rejected — would limit the history page's debugging use cases; users need to filter by task, status, dashboard, environment, and date range.\n@router.get(\"/runs\", response_model=ValidationRunListResponse)\nasync def list_runs(\n task_id: str | None = Query(None, description=\"Filter by spawned task ID\"),\n dashboard_id: str | None = Query(None, description=\"Filter by dashboard ID\"),\n environment_id: str | None = Query(None, description=\"Filter by environment ID\"),\n status: str | None = Query(None, description=\"Filter by status: PASS, WARN, FAIL, UNKNOWN\"),\n date_from: date | None = Query(None, description=\"Start date (ISO format)\"),\n date_to: date | None = Query(None, description=\"End date (ISO format)\"),\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(\"validation.run\", \"VIEW\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"List validation runs with cross-task filtering and pagination.\"\"\"\n logger.reason(f\"list_runs — User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n total, items = service.list_runs(\n task_id=task_id, dashboard_id=dashboard_id, environment_id=environment_id,\n status=status, date_from=date_from, date_to=date_to, page=page, page_size=page_size,\n )\n return ValidationRunListResponse(items=items, total=total, page=page, page_size=page_size)\n except ValueError as e:\n raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))\n# #endregion list_runs\n\n\n# #region get_run_detail [C:4] [TYPE Function]\n# @RATIONALE Returns full run detail including issues list and raw response for the report view — single endpoint serves the complete detail page without multiple fetches.\n# @REJECTED Paginated issues endpoint rejected — issues per run are typically < 20 items; pagination overhead is unnecessary for such small collections.\n@router.get(\"/runs/{run_id}\", response_model=ValidationRunDetailResponse)\nasync def get_run_detail(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.run\", \"VIEW\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Get detailed validation run info including issues and raw response.\"\"\"\n logger.reason(f\"get_run_detail — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n try:\n return service.get_run_detail(run_id)\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# #region delete_run [C:3] [TYPE Function]\n# @RATIONALE Returns 404 when run not found — explicit error handling rather than silent success; helps frontend distinguish between successful delete and missing resource.\n# @REJECTED Silent success on missing run rejected — would mask bugs in the UI that reference already-deleted runs; explicit 404 helps debugging.\n@router.delete(\"/runs/{run_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def delete_run(\n run_id: str,\n current_user: User = Depends(get_current_user),\n _=Depends(has_permission(\"validation.run\", \"DELETE\")),\n service: ValidationRunService = Depends(_get_run_service),\n):\n \"\"\"Delete a validation run record.\"\"\"\n logger.reason(f\"delete_run — Run: {run_id}, User: {current_user.username}\", extra={\"src\": \"validation_routes\"})\n if not service.delete_run(run_id):\n raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"Run not found\")\n# #endregion delete_run\n\n\n# #endregion ValidationRoutes\n" }, { "contract_id": "_get_task_service", @@ -15620,8 +16469,8 @@ "contract_id": "update_task", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 138, - "end_line": 158, + "start_line": 137, + "end_line": 157, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -15639,8 +16488,8 @@ "contract_id": "delete_task", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 161, - "end_line": 180, + "start_line": 160, + "end_line": 179, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -15658,8 +16507,8 @@ "contract_id": "trigger_run", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 188, - "end_line": 206, + "start_line": 187, + "end_line": 205, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -15677,8 +16526,8 @@ "contract_id": "list_runs", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 214, - "end_line": 241, + "start_line": 213, + "end_line": 240, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -15696,8 +16545,8 @@ "contract_id": "get_run_detail", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 244, - "end_line": 260, + "start_line": 243, + "end_line": 259, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -15715,8 +16564,8 @@ "contract_id": "delete_run", "contract_type": "Function", "file_path": "backend/src/api/routes/validation.py", - "start_line": 263, - "end_line": 277, + "start_line": 262, + "end_line": 276, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -15764,7 +16613,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationApiTests [C:3] [TYPE Module] [SEMANTICS validation, api, tests, pagination, crud]\n# @BRIEF Unit tests for validation task CRUD and run history API endpoints.\n# @LAYER API\n# @RELATION BINDS_TO -> [ValidationRoutes]\n# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]\n# @TEST_CONTRACT [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]\n# @TEST_SCENARIO list_tasks -> 200 with paginated tasks; filter by is_active/environment_id\n# @TEST_SCENARIO create_task -> 201 with valid payload; 422 with invalid provider/environment\n# @TEST_SCENARIO get_task -> 200 with task+recent_runs; 404 for nonexistent\n# @TEST_SCENARIO update_task -> 200 with updated fields; 404 for nonexistent\n# @TEST_SCENARIO delete_task -> 204 on success; 404 for nonexistent\n# @TEST_SCENARIO trigger_run -> 200 with spawned_task_id; 422 for invalid task\n# @TEST_SCENARIO list_runs -> 200 with 6 filters + pagination\n# @TEST_SCENARIO get_run_detail -> 200 with issues/raw_response; 404 for nonexistent\n# @TEST_SCENARIO delete_run -> 204 on success; 404 for nonexistent\n# @TEST_EDGE missing_field -> 422 when required field omitted from create payload\n# @TEST_EDGE invalid_type -> 422 when provider_id is not multimodal\n# @TEST_EDGE external_fail -> 404/422 when task/run does not exist\n# @TEST_EDGE delete_task_with_runs -> 204 with delete_runs=true flag\n# @INVARIANT Every endpoint requires authentication via has_permission\n# @INVARIANT All list endpoints support pagination (page/page_size) with defaults\n# @INVARIANT provider_id must reference a multimodal LLM provider for creation\n\n# Set required env vars before ANY app imports — crash-early guard for AuthConfig()\nimport os\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", \"sqlite:///:memory:test_auth\")\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:///:memory:test_main\")\nos.environ.setdefault(\"DEV_MODE\", \"true\")\n\nfrom datetime import datetime, timezone\n\nimport pytest\nfrom unittest.mock import AsyncMock, MagicMock\n\nfrom fastapi.testclient import TestClient\n\nfrom src.app import app\nfrom src.dependencies import (\n get_current_user,\n get_config_manager,\n get_task_manager,\n)\nfrom src.api.routes.validation import _get_task_service, _get_run_service\nfrom src.schemas.validation import (\n ValidationTaskListResponse,\n ValidationTaskResponse,\n ValidationRunListResponse,\n ValidationRunDetailResponse,\n TriggerRunResponse,\n)\nfrom src.models.auth import User\n\n# ── Shared mocks ───────────────────────────────────────────────────────\n\nmock_user = MagicMock(spec=User)\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nadmin_role.permissions = []\nmock_user.roles.append(admin_role)\n\n\ndef _make_now():\n \"\"\"Return a timezone-aware UTC datetime for Pydantic v2.\"\"\"\n return datetime.now(timezone.utc)\n\n\n@pytest.fixture(autouse=True)\ndef mock_all_deps():\n \"\"\"Override service-level FastAPI dependencies with MagicMock stubs.\n\n Permission checking (has_permission) is NOT overridden — mock_user has\n Admin role which passes all permission gates implicitly.\n Fixture resets mock_user.roles before each test to prevent cross-test pollution.\n \"\"\"\n # Reset user state — Admin role passes all permission gates\n mock_user.roles = [admin_role]\n\n mock_task_service = MagicMock()\n mock_run_service = MagicMock()\n mock_task_manager = MagicMock()\n\n app.dependency_overrides[_get_task_service] = lambda: mock_task_service\n app.dependency_overrides[_get_run_service] = lambda: mock_run_service\n app.dependency_overrides[get_config_manager] = lambda: MagicMock()\n app.dependency_overrides[get_task_manager] = lambda: mock_task_manager\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n yield {\n \"task_service\": mock_task_service,\n \"run_service\": mock_run_service,\n \"task_manager\": mock_task_manager,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n\n# ── Fixtures ────────────────────────────────────────────────────────────\n\ndef _make_task_response(**overrides):\n \"\"\"Build a ValidationTaskResponse for mock returns.\"\"\"\n return ValidationTaskResponse(\n id=overrides.get(\"id\", \"task-1\"),\n name=overrides.get(\"name\", \"Test Task\"),\n environment_id=overrides.get(\"environment_id\", \"env-1\"),\n is_active=overrides.get(\"is_active\", True),\n dashboard_ids=overrides.get(\"dashboard_ids\", [\"dash-1\"]),\n provider_id=overrides.get(\"provider_id\", \"provider-1\"),\n schedule_days=None,\n window_start=None,\n window_end=None,\n notify_owners=True,\n custom_channels=None,\n alert_condition=\"FAIL_ONLY\",\n created_at=overrides.get(\"created_at\", _make_now()),\n updated_at=None,\n last_run_status=None,\n last_run_at=None,\n )\n\n\ndef _make_run_response(**overrides):\n \"\"\"Build a dict representation of a ValidationRunResponse.\"\"\"\n return {\n \"id\": overrides.get(\"id\", \"run-1\"),\n \"task_id\": overrides.get(\"task_id\", \"task-1\"),\n \"dashboard_id\": overrides.get(\"dashboard_id\", \"dash-1\"),\n \"environment_id\": overrides.get(\"environment_id\", \"env-1\"),\n \"status\": overrides.get(\"status\", \"PASS\"),\n \"summary\": overrides.get(\"summary\", \"\"),\n \"timestamp\": overrides.get(\"timestamp\", _make_now()),\n \"screenshot_path\": None,\n \"task_name\": overrides.get(\"task_name\", \"Test Task\"),\n }\n\n\n# ── Task CRUD Tests ────────────────────────────────────────────────────\n\n# #region test_list_tasks_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/tasks returns paginated task list with filters.\ndef test_list_tasks_success(mock_all_deps):\n task = _make_task_response()\n mock_all_deps[\"task_service\"].list_tasks.return_value = (1, [task])\n\n response = client.get(\"/api/validation/tasks\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"items\"][0][\"name\"] == \"Test Task\"\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n ValidationTaskListResponse(**data)\n\n # environment_id filter\n response = client.get(\"/api/validation/tasks?environment_id=env-1\")\n assert response.status_code == 200\n mock_all_deps[\"task_service\"].list_tasks.assert_called_with(\n is_active=None, environment_id=\"env-1\", page=1, page_size=20\n )\n# #endregion test_list_tasks_success\n\n\n# #region test_list_tasks_pagination [C:2] [TYPE Function]\n# @BRIEF Pagination query params are validated: page >= 1, page_size 1-100.\ndef test_list_tasks_pagination(mock_all_deps):\n response = client.get(\"/api/validation/tasks?page=0\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/tasks?page_size=101\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/tasks?page_size=0\")\n assert response.status_code == 422\n# #endregion test_list_tasks_pagination\n\n\n# #region test_list_tasks_service_error [C:2] [TYPE Function]\n# @BRIEF Service ValueError becomes 400 on list_tasks.\ndef test_list_tasks_service_error(mock_all_deps):\n mock_all_deps[\"task_service\"].list_tasks.side_effect = ValueError(\"Bad filter\")\n response = client.get(\"/api/validation/tasks?environment_id=nonexistent\")\n assert response.status_code == 400\n assert \"Bad filter\" in response.json()[\"detail\"]\n# #endregion test_list_tasks_service_error\n\n\n# #region test_create_task_success [C:2] [TYPE Function]\n# @BRIEF POST /validation/tasks with valid payload returns 201.\ndef test_create_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].create_task.return_value = _make_task_response()\n\n payload = {\n \"name\": \"My Validation Task\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"dash-1\"],\n \"provider_id\": \"provider-1\",\n \"notify_owners\": True,\n \"alert_condition\": \"FAIL_ONLY\",\n }\n response = client.post(\"/api/validation/tasks\", json=payload)\n assert response.status_code == 201\n data = response.json()\n assert data[\"name\"] == \"Test Task\"\n assert data[\"id\"] == \"task-1\"\n ValidationTaskResponse(**data)\n mock_all_deps[\"task_service\"].create_task.assert_called_once()\n# #endregion test_create_task_success\n\n\n# #region test_create_task_missing_fields [C:2] [TYPE Function]\n# @BRIEF POST with missing required fields returns 422.\ndef test_create_task_missing_fields(mock_all_deps):\n # Empty payload\n response = client.post(\"/api/validation/tasks\", json={})\n assert response.status_code == 422\n errors = response.json()\n assert \"detail\" in errors\n\n # Missing provider_id\n response = client.post(\"/api/validation/tasks\", json={\n \"name\": \"Test\", \"environment_id\": \"env-1\", \"dashboard_ids\": [\"dash-1\"],\n })\n assert response.status_code == 422\n\n # Missing dashboard_ids\n response = client.post(\"/api/validation/tasks\", json={\n \"name\": \"Test\", \"environment_id\": \"env-1\", \"provider_id\": \"provider-1\",\n })\n assert response.status_code == 422\n# #endregion test_create_task_missing_fields\n\n\n# #region test_create_task_invalid_provider [C:2] [TYPE Function]\n# @BRIEF POST with non-multimodal provider returns 422.\ndef test_create_task_invalid_provider(mock_all_deps):\n mock_all_deps[\"task_service\"].create_task.side_effect = ValueError(\n \"LLM provider 'GPT-3.5' is not multimodal\"\n )\n payload = {\n \"name\": \"Test\", \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"dash-1\"], \"provider_id\": \"provider-2\",\n }\n response = client.post(\"/api/validation/tasks\", json=payload)\n assert response.status_code == 422\n assert \"not multimodal\" in response.json()[\"detail\"]\n# #endregion test_create_task_invalid_provider\n\n\n# #region test_get_task_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/tasks/{id} returns task with recent_runs.\ndef test_get_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].get_task.return_value = _make_task_response()\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n\n response = client.get(\"/api/validation/tasks/task-1\")\n assert response.status_code == 200\n data = response.json()\n assert \"task\" in data\n assert \"recent_runs\" in data\n assert data[\"task\"][\"name\"] == \"Test Task\"\n mock_all_deps[\"task_service\"].get_task.assert_called_with(\"task-1\")\n# #endregion test_get_task_success\n\n\n# #region test_get_task_not_found [C:2] [TYPE Function]\n# @BRIEF GET for nonexistent task returns 404.\ndef test_get_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].get_task.side_effect = ValueError(\"not found\")\n response = client.get(\"/api/validation/tasks/nonexistent\")\n assert response.status_code == 404\n# #endregion test_get_task_not_found\n\n\n# #region test_update_task_success [C:2] [TYPE Function]\n# @BRIEF PUT /validation/tasks/{id} updates task and returns updated object.\ndef test_update_task_success(mock_all_deps):\n updated = _make_task_response(name=\"Updated Name\", is_active=False)\n mock_all_deps[\"task_service\"].update_task.return_value = updated\n\n payload = {\"name\": \"Updated Name\", \"is_active\": False}\n response = client.put(\"/api/validation/tasks/task-1\", json=payload)\n assert response.status_code == 200\n data = response.json()\n assert data[\"name\"] == \"Updated Name\"\n assert data[\"is_active\"] is False\n ValidationTaskResponse(**data)\n mock_all_deps[\"task_service\"].update_task.assert_called_once()\n# #endregion test_update_task_success\n\n\n# #region test_update_task_not_found [C:2] [TYPE Function]\n# @BRIEF PUT for nonexistent task returns 404.\ndef test_update_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].update_task.side_effect = ValueError(\"not found\")\n payload = {\"name\": \"Nope\"}\n response = client.put(\"/api/validation/tasks/nonexistent\", json=payload)\n assert response.status_code == 404\n# #endregion test_update_task_not_found\n\n\n# #region test_delete_task_success [C:2] [TYPE Function]\n# @BRIEF DELETE /validation/tasks/{id} returns 204.\ndef test_delete_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=False)\n# #endregion test_delete_task_success\n\n\n# #region test_delete_task_not_found [C:2] [TYPE Function]\n# @BRIEF DELETE for nonexistent task returns 404.\ndef test_delete_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.side_effect = ValueError(\"not found\")\n response = client.delete(\"/api/validation/tasks/nonexistent\")\n assert response.status_code == 404\n# #endregion test_delete_task_not_found\n\n\n# #region test_delete_task_with_runs_flag [C:2] [TYPE Function]\n# @BRIEF DELETE with delete_runs=true passes query param to service.\ndef test_delete_task_with_runs_flag(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1?delete_runs=true\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=True)\n# #endregion test_delete_task_with_runs_flag\n\n\n# #region test_trigger_run_success [C:2] [TYPE Function]\n# @BRIEF POST /validation/tasks/{id}/run spawns a validation task.\ndef test_trigger_run_success(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(return_value=\"spawned-1\")\n mock_all_deps[\"task_manager\"].create_task = AsyncMock()\n\n response = client.post(\"/api/validation/tasks/task-1/run\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"task_id\"] == \"task-1\"\n assert data[\"spawned_task_id\"] == \"spawned-1\"\n assert data[\"status\"] == \"PENDING\"\n TriggerRunResponse(**data)\n# #endregion test_trigger_run_success\n\n\n# #region test_trigger_run_not_found [C:2] [TYPE Function]\n# @BRIEF POST for nonexistent task returns 422.\ndef test_trigger_run_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(\n side_effect=ValueError(\"Validation task 'nonexistent' not found\")\n )\n response = client.post(\"/api/validation/tasks/nonexistent/run\")\n assert response.status_code == 422\n assert \"not found\" in response.json()[\"detail\"]\n# #endregion test_trigger_run_not_found\n\n\n# #region test_trigger_run_no_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger fails with 422 when task has no dashboard IDs.\ndef test_trigger_run_no_dashboards(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(\n side_effect=ValueError(\"has no dashboard IDs configured\")\n )\n response = client.post(\"/api/validation/tasks/task-1/run\")\n assert response.status_code == 422\n assert \"no dashboard IDs\" in response.json()[\"detail\"]\n# #endregion test_trigger_run_no_dashboards\n\n\n# ── Run History Tests ──────────────────────────────────────────────────\n\n# #region test_list_runs_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/runs returns paginated run list.\ndef test_list_runs_success(mock_all_deps):\n run = _make_run_response()\n mock_all_deps[\"run_service\"].list_runs.return_value = (1, [run])\n\n response = client.get(\"/api/validation/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"items\"][0][\"id\"] == \"run-1\"\n assert data[\"total\"] == 1\n ValidationRunListResponse(**data)\n# #endregion test_list_runs_success\n\n\n# #region test_list_runs_filters [C:2] [TYPE Function]\n# @BRIEF All 6 filter query params are accepted and forwarded to service.\ndef test_list_runs_filters(mock_all_deps):\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n\n for query in [\n \"?task_id=task-1\",\n \"?dashboard_id=dash-1\",\n \"?environment_id=env-1\",\n \"?status=PASS\",\n \"?date_from=2026-01-01\",\n \"?date_to=2026-12-31\",\n \"?status=PASS&dashboard_id=dash-1&date_from=2026-01-01\",\n ]:\n response = client.get(f\"/api/validation/runs{query}\")\n assert response.status_code == 200, f\"Filter query failed: {query}\"\n# #endregion test_list_runs_filters\n\n\n# #region test_list_runs_pagination [C:2] [TYPE Function]\n# @BRIEF Pagination boundary checks on runs list.\ndef test_list_runs_pagination(mock_all_deps):\n response = client.get(\"/api/validation/runs?page=0\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/runs?page_size=101\")\n assert response.status_code == 422\n# #endregion test_list_runs_pagination\n\n\n# #region test_list_runs_invalid_status [C:2] [TYPE Function]\n# @BRIEF Status is a free string; any value passes through to service.\ndef test_list_runs_invalid_status(mock_all_deps):\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n response = client.get(\"/api/validation/runs?status=INVALID_STATUS_XYZ\")\n assert response.status_code == 200\n call_kwargs = mock_all_deps[\"run_service\"].list_runs.call_args[1]\n assert call_kwargs.get(\"status\") == \"INVALID_STATUS_XYZ\"\n# #endregion test_list_runs_invalid_status\n\n\n# #region test_get_run_detail_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/runs/{id} returns full detail with issues and raw_response.\ndef test_get_run_detail_success(mock_all_deps):\n mock_all_deps[\"run_service\"].get_run_detail.return_value = ValidationRunDetailResponse(\n id=\"run-1\",\n task_id=\"task-1\",\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL\",\n summary=\"Charts failed to render\",\n timestamp=_make_now(),\n screenshot_path=\"/tmp/screenshots/run-1.png\",\n issues=[{\"severity\": \"HIGH\", \"message\": \"Chart timeout\"}],\n raw_response='{\"status\": \"error\"}',\n task_name=\"Test Task\",\n )\n\n response = client.get(\"/api/validation/runs/run-1\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"FAIL\"\n assert len(data[\"issues\"]) == 1\n assert data[\"raw_response\"] is not None\n ValidationRunDetailResponse(**data)\n# #endregion test_get_run_detail_success\n\n\n# #region test_get_run_detail_not_found [C:2] [TYPE Function]\n# @BRIEF GET for nonexistent run returns 404.\ndef test_get_run_detail_not_found(mock_all_deps):\n mock_all_deps[\"run_service\"].get_run_detail.side_effect = ValueError(\"not found\")\n response = client.get(\"/api/validation/runs/nonexistent\")\n assert response.status_code == 404\n# #endregion test_get_run_detail_not_found\n\n\n# #region test_delete_run_success [C:2] [TYPE Function]\n# @BRIEF DELETE /validation/runs/{id} returns 204.\ndef test_delete_run_success(mock_all_deps):\n mock_all_deps[\"run_service\"].delete_run.return_value = True\n response = client.delete(\"/api/validation/runs/run-1\")\n assert response.status_code == 204\n mock_all_deps[\"run_service\"].delete_run.assert_called_with(\"run-1\")\n# #endregion test_delete_run_success\n\n\n# #region test_delete_run_not_found [C:2] [TYPE Function]\n# @BRIEF DELETE for nonexistent run returns 404.\ndef test_delete_run_not_found(mock_all_deps):\n mock_all_deps[\"run_service\"].delete_run.return_value = False\n response = client.delete(\"/api/validation/runs/nonexistent\")\n assert response.status_code == 404\n# #endregion test_delete_run_not_found\n\n\n# ── Auth / Permission Tests ────────────────────────────────────────────\n\n# #region test_endpoints_require_authentication [C:2] [TYPE Function]\n# @BRIEF Without current_user dependency, all 9 endpoints return 401.\ndef test_endpoints_require_authentication(mock_all_deps):\n app.dependency_overrides.pop(get_current_user, None)\n endpoints = [\n (\"GET\", \"/api/validation/tasks\"),\n (\"POST\", \"/api/validation/tasks\"),\n (\"GET\", \"/api/validation/tasks/task-1\"),\n (\"PUT\", \"/api/validation/tasks/task-1\"),\n (\"DELETE\", \"/api/validation/tasks/task-1\"),\n (\"POST\", \"/api/validation/tasks/task-1/run\"),\n (\"GET\", \"/api/validation/runs\"),\n (\"GET\", \"/api/validation/runs/run-1\"),\n (\"DELETE\", \"/api/validation/runs/run-1\"),\n ]\n for method, path in endpoints:\n response = client.request(method, path)\n assert response.status_code == 401, f\"{method} {path} expected 401 got {response.status_code}\"\n# #endregion test_endpoints_require_authentication\n\n\n# #region test_permission_denied_non_admin [C:2] [TYPE Function]\n# @BRIEF A non-admin user without validation.* permissions receives 403.\ndef test_permission_denied_non_admin(mock_all_deps):\n \"\"\"Remove Admin role so has_permission raises 403.\"\"\"\n mock_user.roles = [] # No Admin, no permissions\n response = client.get(\"/api/validation/tasks\")\n assert response.status_code == 403\n# #endregion test_permission_denied_non_admin\n\n\n# #region test_permission_denied_all_actions [C:2] [TYPE Function]\n# @BRIEF Each action (VIEW, CREATE, EDIT, DELETE, EXECUTE) denied for non-admin.\ndef test_permission_denied_all_actions(mock_all_deps):\n mock_user.roles = []\n action_endpoints = [\n (\"GET\", \"/api/validation/tasks\"),\n (\"POST\", \"/api/validation/tasks\"),\n (\"GET\", \"/api/validation/tasks/task-1\"),\n (\"PUT\", \"/api/validation/tasks/task-1\"),\n (\"DELETE\", \"/api/validation/tasks/task-1\"),\n (\"POST\", \"/api/validation/tasks/task-1/run\"),\n (\"GET\", \"/api/validation/runs\"),\n (\"GET\", \"/api/validation/runs/run-1\"),\n (\"DELETE\", \"/api/validation/runs/run-1\"),\n ]\n for method, path in action_endpoints:\n response = client.request(method, path)\n assert response.status_code == 403, f\"{method} {path} expected 403 got {response.status_code}\"\n# #endregion test_permission_denied_all_actions\n\n\n# #region test_delete_task_runs_default_false [C:2] [TYPE Function]\n# @BRIEF delete_runs query param defaults to False.\ndef test_delete_task_runs_default_false(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=False)\n# #endregion test_delete_task_runs_default_false\n\n# #endregion ValidationApiTests\n" + "body": "# #region ValidationApiTests [C:3] [TYPE Module] [SEMANTICS validation, api, tests, pagination, crud]\n# @BRIEF Unit tests for validation task CRUD and run history API endpoints.\n# @LAYER API\n# @RELATION BINDS_TO -> [ValidationRoutes]\n# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]\n# @TEST_CONTRACT [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]\n# @TEST_SCENARIO list_tasks -> 200 with paginated tasks; filter by is_active/environment_id\n# @TEST_SCENARIO create_task -> 201 with valid payload; 422 with invalid provider/environment\n# @TEST_SCENARIO get_task -> 200 with task+recent_runs; 404 for nonexistent\n# @TEST_SCENARIO update_task -> 200 with updated fields; 404 for nonexistent\n# @TEST_SCENARIO delete_task -> 204 on success; 404 for nonexistent\n# @TEST_SCENARIO trigger_run -> 200 with spawned_task_id; 422 for invalid task\n# @TEST_SCENARIO list_runs -> 200 with 6 filters + pagination\n# @TEST_SCENARIO get_run_detail -> 200 with issues/raw_response; 404 for nonexistent\n# @TEST_SCENARIO delete_run -> 204 on success; 404 for nonexistent\n# @TEST_EDGE missing_field -> 422 when required field omitted from create payload\n# @TEST_EDGE invalid_type -> 422 when provider_id is not multimodal\n# @TEST_EDGE external_fail -> 404/422 when task/run does not exist\n# @TEST_EDGE delete_task_with_runs -> 204 with delete_runs=true flag\n# @INVARIANT Every endpoint requires authentication via has_permission\n# @INVARIANT All list endpoints support pagination (page/page_size) with defaults\n# @INVARIANT provider_id must reference a multimodal LLM provider for creation\n\n# Set required env vars before ANY app imports — crash-early guard for AuthConfig()\nimport os\n\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", \"sqlite:///:memory:test_auth\")\nos.environ.setdefault(\"DATABASE_URL\", \"sqlite:///:memory:test_main\")\nos.environ.setdefault(\"DEV_MODE\", \"true\")\n\nfrom datetime import UTC, datetime\nimport pytest\nfrom unittest.mock import AsyncMock, MagicMock\n\nfrom fastapi.testclient import TestClient\n\nfrom src.api.routes.validation import _get_run_service, _get_task_service\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_task_manager,\n)\nfrom src.models.auth import User\nfrom src.schemas.validation import (\n TriggerRunResponse,\n ValidationRunDetailResponse,\n ValidationRunListResponse,\n ValidationTaskListResponse,\n ValidationTaskResponse,\n)\n\n# ── Shared mocks ───────────────────────────────────────────────────────\n\nmock_user = MagicMock(spec=User)\nmock_user.username = \"testuser\"\nmock_user.roles = []\nadmin_role = MagicMock()\nadmin_role.name = \"Admin\"\nadmin_role.permissions = []\nmock_user.roles.append(admin_role)\n\n\ndef _make_now():\n \"\"\"Return a timezone-aware UTC datetime for Pydantic v2.\"\"\"\n return datetime.now(UTC)\n\n\n@pytest.fixture(autouse=True)\ndef mock_all_deps():\n \"\"\"Override service-level FastAPI dependencies with MagicMock stubs.\n\n Permission checking (has_permission) is NOT overridden — mock_user has\n Admin role which passes all permission gates implicitly.\n Fixture resets mock_user.roles before each test to prevent cross-test pollution.\n \"\"\"\n # Reset user state — Admin role passes all permission gates\n mock_user.roles = [admin_role]\n\n mock_task_service = MagicMock()\n mock_run_service = MagicMock()\n mock_task_manager = MagicMock()\n\n app.dependency_overrides[_get_task_service] = lambda: mock_task_service\n app.dependency_overrides[_get_run_service] = lambda: mock_run_service\n app.dependency_overrides[get_config_manager] = lambda: MagicMock()\n app.dependency_overrides[get_task_manager] = lambda: mock_task_manager\n app.dependency_overrides[get_current_user] = lambda: mock_user\n\n yield {\n \"task_service\": mock_task_service,\n \"run_service\": mock_run_service,\n \"task_manager\": mock_task_manager,\n }\n app.dependency_overrides.clear()\n\n\nclient = TestClient(app)\n\n\n# ── Fixtures ────────────────────────────────────────────────────────────\n\ndef _make_task_response(**overrides):\n \"\"\"Build a ValidationTaskResponse for mock returns.\"\"\"\n return ValidationTaskResponse(\n id=overrides.get(\"id\", \"task-1\"),\n name=overrides.get(\"name\", \"Test Task\"),\n environment_id=overrides.get(\"environment_id\", \"env-1\"),\n is_active=overrides.get(\"is_active\", True),\n dashboard_ids=overrides.get(\"dashboard_ids\", [\"dash-1\"]),\n provider_id=overrides.get(\"provider_id\", \"provider-1\"),\n schedule_days=None,\n window_start=None,\n window_end=None,\n notify_owners=True,\n custom_channels=None,\n alert_condition=\"FAIL_ONLY\",\n created_at=overrides.get(\"created_at\", _make_now()),\n updated_at=None,\n last_run_status=None,\n last_run_at=None,\n )\n\n\ndef _make_run_response(**overrides):\n \"\"\"Build a dict representation of a ValidationRunResponse.\"\"\"\n return {\n \"id\": overrides.get(\"id\", \"run-1\"),\n \"task_id\": overrides.get(\"task_id\", \"task-1\"),\n \"dashboard_id\": overrides.get(\"dashboard_id\", \"dash-1\"),\n \"environment_id\": overrides.get(\"environment_id\", \"env-1\"),\n \"status\": overrides.get(\"status\", \"PASS\"),\n \"summary\": overrides.get(\"summary\", \"\"),\n \"timestamp\": overrides.get(\"timestamp\", _make_now()),\n \"screenshot_path\": None,\n \"task_name\": overrides.get(\"task_name\", \"Test Task\"),\n }\n\n\n# ── Task CRUD Tests ────────────────────────────────────────────────────\n\n# #region test_list_tasks_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/tasks returns paginated task list with filters.\ndef test_list_tasks_success(mock_all_deps):\n task = _make_task_response()\n mock_all_deps[\"task_service\"].list_tasks.return_value = (1, [task])\n\n response = client.get(\"/api/validation/tasks\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"items\"][0][\"name\"] == \"Test Task\"\n assert data[\"total\"] == 1\n assert data[\"page\"] == 1\n assert data[\"page_size\"] == 20\n ValidationTaskListResponse(**data)\n\n # environment_id filter\n response = client.get(\"/api/validation/tasks?environment_id=env-1\")\n assert response.status_code == 200\n mock_all_deps[\"task_service\"].list_tasks.assert_called_with(\n is_active=None, environment_id=\"env-1\", page=1, page_size=20\n )\n# #endregion test_list_tasks_success\n\n\n# #region test_list_tasks_pagination [C:2] [TYPE Function]\n# @BRIEF Pagination query params are validated: page >= 1, page_size 1-100.\ndef test_list_tasks_pagination(mock_all_deps):\n response = client.get(\"/api/validation/tasks?page=0\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/tasks?page_size=101\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/tasks?page_size=0\")\n assert response.status_code == 422\n# #endregion test_list_tasks_pagination\n\n\n# #region test_list_tasks_service_error [C:2] [TYPE Function]\n# @BRIEF Service ValueError becomes 400 on list_tasks.\ndef test_list_tasks_service_error(mock_all_deps):\n mock_all_deps[\"task_service\"].list_tasks.side_effect = ValueError(\"Bad filter\")\n response = client.get(\"/api/validation/tasks?environment_id=nonexistent\")\n assert response.status_code == 400\n assert \"Bad filter\" in response.json()[\"detail\"]\n# #endregion test_list_tasks_service_error\n\n\n# #region test_create_task_success [C:2] [TYPE Function]\n# @BRIEF POST /validation/tasks with valid payload returns 201.\ndef test_create_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].create_task.return_value = _make_task_response()\n\n payload = {\n \"name\": \"My Validation Task\",\n \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"dash-1\"],\n \"provider_id\": \"provider-1\",\n \"notify_owners\": True,\n \"alert_condition\": \"FAIL_ONLY\",\n }\n response = client.post(\"/api/validation/tasks\", json=payload)\n assert response.status_code == 201\n data = response.json()\n assert data[\"name\"] == \"Test Task\"\n assert data[\"id\"] == \"task-1\"\n ValidationTaskResponse(**data)\n mock_all_deps[\"task_service\"].create_task.assert_called_once()\n# #endregion test_create_task_success\n\n\n# #region test_create_task_missing_fields [C:2] [TYPE Function]\n# @BRIEF POST with missing required fields returns 422.\ndef test_create_task_missing_fields(mock_all_deps):\n # Empty payload\n response = client.post(\"/api/validation/tasks\", json={})\n assert response.status_code == 422\n errors = response.json()\n assert \"detail\" in errors\n\n # Missing provider_id\n response = client.post(\"/api/validation/tasks\", json={\n \"name\": \"Test\", \"environment_id\": \"env-1\", \"dashboard_ids\": [\"dash-1\"],\n })\n assert response.status_code == 422\n\n # Missing dashboard_ids\n response = client.post(\"/api/validation/tasks\", json={\n \"name\": \"Test\", \"environment_id\": \"env-1\", \"provider_id\": \"provider-1\",\n })\n assert response.status_code == 422\n# #endregion test_create_task_missing_fields\n\n\n# #region test_create_task_invalid_provider [C:2] [TYPE Function]\n# @BRIEF POST with non-multimodal provider returns 422.\ndef test_create_task_invalid_provider(mock_all_deps):\n mock_all_deps[\"task_service\"].create_task.side_effect = ValueError(\n \"LLM provider 'GPT-3.5' is not multimodal\"\n )\n payload = {\n \"name\": \"Test\", \"environment_id\": \"env-1\",\n \"dashboard_ids\": [\"dash-1\"], \"provider_id\": \"provider-2\",\n }\n response = client.post(\"/api/validation/tasks\", json=payload)\n assert response.status_code == 422\n assert \"not multimodal\" in response.json()[\"detail\"]\n# #endregion test_create_task_invalid_provider\n\n\n# #region test_get_task_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/tasks/{id} returns task with recent_runs.\ndef test_get_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].get_task.return_value = _make_task_response()\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n\n response = client.get(\"/api/validation/tasks/task-1\")\n assert response.status_code == 200\n data = response.json()\n assert \"task\" in data\n assert \"recent_runs\" in data\n assert data[\"task\"][\"name\"] == \"Test Task\"\n mock_all_deps[\"task_service\"].get_task.assert_called_with(\"task-1\")\n# #endregion test_get_task_success\n\n\n# #region test_get_task_not_found [C:2] [TYPE Function]\n# @BRIEF GET for nonexistent task returns 404.\ndef test_get_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].get_task.side_effect = ValueError(\"not found\")\n response = client.get(\"/api/validation/tasks/nonexistent\")\n assert response.status_code == 404\n# #endregion test_get_task_not_found\n\n\n# #region test_update_task_success [C:2] [TYPE Function]\n# @BRIEF PUT /validation/tasks/{id} updates task and returns updated object.\ndef test_update_task_success(mock_all_deps):\n updated = _make_task_response(name=\"Updated Name\", is_active=False)\n mock_all_deps[\"task_service\"].update_task.return_value = updated\n\n payload = {\"name\": \"Updated Name\", \"is_active\": False}\n response = client.put(\"/api/validation/tasks/task-1\", json=payload)\n assert response.status_code == 200\n data = response.json()\n assert data[\"name\"] == \"Updated Name\"\n assert data[\"is_active\"] is False\n ValidationTaskResponse(**data)\n mock_all_deps[\"task_service\"].update_task.assert_called_once()\n# #endregion test_update_task_success\n\n\n# #region test_update_task_not_found [C:2] [TYPE Function]\n# @BRIEF PUT for nonexistent task returns 404.\ndef test_update_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].update_task.side_effect = ValueError(\"not found\")\n payload = {\"name\": \"Nope\"}\n response = client.put(\"/api/validation/tasks/nonexistent\", json=payload)\n assert response.status_code == 404\n# #endregion test_update_task_not_found\n\n\n# #region test_delete_task_success [C:2] [TYPE Function]\n# @BRIEF DELETE /validation/tasks/{id} returns 204.\ndef test_delete_task_success(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=False)\n# #endregion test_delete_task_success\n\n\n# #region test_delete_task_not_found [C:2] [TYPE Function]\n# @BRIEF DELETE for nonexistent task returns 404.\ndef test_delete_task_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.side_effect = ValueError(\"not found\")\n response = client.delete(\"/api/validation/tasks/nonexistent\")\n assert response.status_code == 404\n# #endregion test_delete_task_not_found\n\n\n# #region test_delete_task_with_runs_flag [C:2] [TYPE Function]\n# @BRIEF DELETE with delete_runs=true passes query param to service.\ndef test_delete_task_with_runs_flag(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1?delete_runs=true\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=True)\n# #endregion test_delete_task_with_runs_flag\n\n\n# #region test_trigger_run_success [C:2] [TYPE Function]\n# @BRIEF POST /validation/tasks/{id}/run spawns a validation task.\ndef test_trigger_run_success(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(return_value=\"spawned-1\")\n mock_all_deps[\"task_manager\"].create_task = AsyncMock()\n\n response = client.post(\"/api/validation/tasks/task-1/run\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"task_id\"] == \"task-1\"\n assert data[\"spawned_task_id\"] == \"spawned-1\"\n assert data[\"status\"] == \"PENDING\"\n TriggerRunResponse(**data)\n# #endregion test_trigger_run_success\n\n\n# #region test_trigger_run_not_found [C:2] [TYPE Function]\n# @BRIEF POST for nonexistent task returns 422.\ndef test_trigger_run_not_found(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(\n side_effect=ValueError(\"Validation task 'nonexistent' not found\")\n )\n response = client.post(\"/api/validation/tasks/nonexistent/run\")\n assert response.status_code == 422\n assert \"not found\" in response.json()[\"detail\"]\n# #endregion test_trigger_run_not_found\n\n\n# #region test_trigger_run_no_dashboards [C:2] [TYPE Function]\n# @BRIEF Trigger fails with 422 when task has no dashboard IDs.\ndef test_trigger_run_no_dashboards(mock_all_deps):\n mock_all_deps[\"task_service\"].trigger_run = AsyncMock(\n side_effect=ValueError(\"has no dashboard IDs configured\")\n )\n response = client.post(\"/api/validation/tasks/task-1/run\")\n assert response.status_code == 422\n assert \"no dashboard IDs\" in response.json()[\"detail\"]\n# #endregion test_trigger_run_no_dashboards\n\n\n# ── Run History Tests ──────────────────────────────────────────────────\n\n# #region test_list_runs_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/runs returns paginated run list.\ndef test_list_runs_success(mock_all_deps):\n run = _make_run_response()\n mock_all_deps[\"run_service\"].list_runs.return_value = (1, [run])\n\n response = client.get(\"/api/validation/runs\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"items\"][0][\"id\"] == \"run-1\"\n assert data[\"total\"] == 1\n ValidationRunListResponse(**data)\n# #endregion test_list_runs_success\n\n\n# #region test_list_runs_filters [C:2] [TYPE Function]\n# @BRIEF All 6 filter query params are accepted and forwarded to service.\ndef test_list_runs_filters(mock_all_deps):\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n\n for query in [\n \"?task_id=task-1\",\n \"?dashboard_id=dash-1\",\n \"?environment_id=env-1\",\n \"?status=PASS\",\n \"?date_from=2026-01-01\",\n \"?date_to=2026-12-31\",\n \"?status=PASS&dashboard_id=dash-1&date_from=2026-01-01\",\n ]:\n response = client.get(f\"/api/validation/runs{query}\")\n assert response.status_code == 200, f\"Filter query failed: {query}\"\n# #endregion test_list_runs_filters\n\n\n# #region test_list_runs_pagination [C:2] [TYPE Function]\n# @BRIEF Pagination boundary checks on runs list.\ndef test_list_runs_pagination(mock_all_deps):\n response = client.get(\"/api/validation/runs?page=0\")\n assert response.status_code == 422\n\n response = client.get(\"/api/validation/runs?page_size=101\")\n assert response.status_code == 422\n# #endregion test_list_runs_pagination\n\n\n# #region test_list_runs_invalid_status [C:2] [TYPE Function]\n# @BRIEF Status is a free string; any value passes through to service.\ndef test_list_runs_invalid_status(mock_all_deps):\n mock_all_deps[\"run_service\"].list_runs.return_value = (0, [])\n response = client.get(\"/api/validation/runs?status=INVALID_STATUS_XYZ\")\n assert response.status_code == 200\n call_kwargs = mock_all_deps[\"run_service\"].list_runs.call_args[1]\n assert call_kwargs.get(\"status\") == \"INVALID_STATUS_XYZ\"\n# #endregion test_list_runs_invalid_status\n\n\n# #region test_get_run_detail_success [C:2] [TYPE Function]\n# @BRIEF GET /validation/runs/{id} returns full detail with issues and raw_response.\ndef test_get_run_detail_success(mock_all_deps):\n mock_all_deps[\"run_service\"].get_run_detail.return_value = ValidationRunDetailResponse(\n id=\"run-1\",\n task_id=\"task-1\",\n dashboard_id=\"dash-1\",\n environment_id=\"env-1\",\n status=\"FAIL\",\n summary=\"Charts failed to render\",\n timestamp=_make_now(),\n screenshot_path=\"/tmp/screenshots/run-1.png\",\n issues=[{\"severity\": \"HIGH\", \"message\": \"Chart timeout\"}],\n raw_response='{\"status\": \"error\"}',\n task_name=\"Test Task\",\n )\n\n response = client.get(\"/api/validation/runs/run-1\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"status\"] == \"FAIL\"\n assert len(data[\"issues\"]) == 1\n assert data[\"raw_response\"] is not None\n ValidationRunDetailResponse(**data)\n# #endregion test_get_run_detail_success\n\n\n# #region test_get_run_detail_not_found [C:2] [TYPE Function]\n# @BRIEF GET for nonexistent run returns 404.\ndef test_get_run_detail_not_found(mock_all_deps):\n mock_all_deps[\"run_service\"].get_run_detail.side_effect = ValueError(\"not found\")\n response = client.get(\"/api/validation/runs/nonexistent\")\n assert response.status_code == 404\n# #endregion test_get_run_detail_not_found\n\n\n# #region test_delete_run_success [C:2] [TYPE Function]\n# @BRIEF DELETE /validation/runs/{id} returns 204.\ndef test_delete_run_success(mock_all_deps):\n mock_all_deps[\"run_service\"].delete_run.return_value = True\n response = client.delete(\"/api/validation/runs/run-1\")\n assert response.status_code == 204\n mock_all_deps[\"run_service\"].delete_run.assert_called_with(\"run-1\")\n# #endregion test_delete_run_success\n\n\n# #region test_delete_run_not_found [C:2] [TYPE Function]\n# @BRIEF DELETE for nonexistent run returns 404.\ndef test_delete_run_not_found(mock_all_deps):\n mock_all_deps[\"run_service\"].delete_run.return_value = False\n response = client.delete(\"/api/validation/runs/nonexistent\")\n assert response.status_code == 404\n# #endregion test_delete_run_not_found\n\n\n# ── Auth / Permission Tests ────────────────────────────────────────────\n\n# #region test_endpoints_require_authentication [C:2] [TYPE Function]\n# @BRIEF Without current_user dependency, all 9 endpoints return 401.\ndef test_endpoints_require_authentication(mock_all_deps):\n app.dependency_overrides.pop(get_current_user, None)\n endpoints = [\n (\"GET\", \"/api/validation/tasks\"),\n (\"POST\", \"/api/validation/tasks\"),\n (\"GET\", \"/api/validation/tasks/task-1\"),\n (\"PUT\", \"/api/validation/tasks/task-1\"),\n (\"DELETE\", \"/api/validation/tasks/task-1\"),\n (\"POST\", \"/api/validation/tasks/task-1/run\"),\n (\"GET\", \"/api/validation/runs\"),\n (\"GET\", \"/api/validation/runs/run-1\"),\n (\"DELETE\", \"/api/validation/runs/run-1\"),\n ]\n for method, path in endpoints:\n response = client.request(method, path)\n assert response.status_code == 401, f\"{method} {path} expected 401 got {response.status_code}\"\n# #endregion test_endpoints_require_authentication\n\n\n# #region test_permission_denied_non_admin [C:2] [TYPE Function]\n# @BRIEF A non-admin user without validation.* permissions receives 403.\ndef test_permission_denied_non_admin(mock_all_deps):\n \"\"\"Remove Admin role so has_permission raises 403.\"\"\"\n mock_user.roles = [] # No Admin, no permissions\n response = client.get(\"/api/validation/tasks\")\n assert response.status_code == 403\n# #endregion test_permission_denied_non_admin\n\n\n# #region test_permission_denied_all_actions [C:2] [TYPE Function]\n# @BRIEF Each action (VIEW, CREATE, EDIT, DELETE, EXECUTE) denied for non-admin.\ndef test_permission_denied_all_actions(mock_all_deps):\n mock_user.roles = []\n action_endpoints = [\n (\"GET\", \"/api/validation/tasks\"),\n (\"POST\", \"/api/validation/tasks\"),\n (\"GET\", \"/api/validation/tasks/task-1\"),\n (\"PUT\", \"/api/validation/tasks/task-1\"),\n (\"DELETE\", \"/api/validation/tasks/task-1\"),\n (\"POST\", \"/api/validation/tasks/task-1/run\"),\n (\"GET\", \"/api/validation/runs\"),\n (\"GET\", \"/api/validation/runs/run-1\"),\n (\"DELETE\", \"/api/validation/runs/run-1\"),\n ]\n for method, path in action_endpoints:\n response = client.request(method, path)\n assert response.status_code == 403, f\"{method} {path} expected 403 got {response.status_code}\"\n# #endregion test_permission_denied_all_actions\n\n\n# #region test_delete_task_runs_default_false [C:2] [TYPE Function]\n# @BRIEF delete_runs query param defaults to False.\ndef test_delete_task_runs_default_false(mock_all_deps):\n mock_all_deps[\"task_service\"].delete_task.return_value = None\n response = client.delete(\"/api/validation/tasks/task-1\")\n assert response.status_code == 204\n mock_all_deps[\"task_service\"].delete_task.assert_called_with(\"task-1\", delete_runs=False)\n# #endregion test_delete_task_runs_default_false\n\n# #endregion ValidationApiTests\n" }, { "contract_id": "test_list_tasks_success", @@ -16275,7 +17124,7 @@ "contract_type": "Module", "file_path": "backend/src/app.py", "start_line": 1, - "end_line": 596, + "end_line": 718, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -16300,14 +17149,52 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AppModule [C:5] [TYPE Module] [SEMANTICS fastapi, websocket, startup, scheduler, middleware]\n# @BRIEF The main entry point for the FastAPI application.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @INVARIANT All WebSocket connections must be authenticated via JWT or API key token (see [SEC:C-3]).\n# @PRE Python environment and dependencies installed; configuration database available.\n# @POST FastAPI app instance is created, middleware configured, and routes registered.\n# @SIDE_EFFECT Starts background scheduler and binds network ports for HTTP/WS traffic.\n# @DATA_CONTRACT [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]\n# @RATIONALE Duplicate @RELATION and @INVARIANT lines removed from header. Import sorting unified via ruff isort (I) rule across src/ — 139 fixes applied.\n\nimport os\nfrom pathlib import Path\n\n# project_root is used for static files mounting\nproject_root = Path(__file__).resolve().parent.parent.parent\nimport asyncio\n\nfrom fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import FileResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.middleware.sessions import SessionMiddleware\n\nfrom .api import auth\nfrom .api.routes import (\n admin,\n admin_api_keys,\n assistant,\n clean_release,\n clean_release_v2,\n dashboards,\n dataset_review,\n datasets,\n environments,\n git,\n health,\n llm,\n maintenance,\n mappings,\n migration,\n plugins,\n profile,\n reports,\n settings,\n storage,\n tasks,\n translate,\n validation,\n)\nfrom .core.auth.security import get_password_hash\nfrom .core.cot_logger import seed_trace_id\nfrom .core.database import AuthSessionLocal, init_db\nfrom .core.encryption_key import ensure_encryption_key\nfrom .core.logger import belief_scope, logger\nfrom .core.utils.network import NetworkError\nfrom .dependencies import get_scheduler_service, get_task_manager\nfrom .models.auth import Role, User\n\n# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n)\n# #endregion FastAPI_App\n# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> [AuthRepository]\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n # Warn about env-var password — visible via /proc to other processes\n logger.warning(\n \"INITIAL_ADMIN_PASSWORD is set via environment variable. \"\n \"This is visible to other processes on the same host. \"\n \"Consider removing the env var after bootstrap.\"\n )\n db = AuthSessionLocal()\n try:\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n admin_role = Role(name=\"Admin\", description=\"System Administrator\", is_admin=True)\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.info(\n \"Initial admin bootstrap skipped: user '%s' already exists.\", username\n )\n return\n new_user = User(\n username=username,\n email=None,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(\n \"Initial admin user '%s' created from environment bootstrap.\", username\n )\n except Exception as exc:\n db.rollback()\n logger.error(\"Failed to bootstrap initial admin user: %s\", exc)\n raise\n finally:\n db.close()\n# #endregion ensure_initial_admin_user\n# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n init_db()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n# #region shutdown_event [C:3] [TYPE Function]\n# @BRIEF Handles application shutdown tasks, such as stopping the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is stopped.\n# Shutdown event\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n seed_trace_id()\n with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n# #endregion shutdown_event\n# #region app_middleware [TYPE Block]\n# @BRIEF Configure application-wide middleware (Session, CORS).\n# @RATIONALE SessionMiddleware uses SESSION_SECRET_KEY independent of JWT SECRET_KEY (see [SEC:H-4]).\n# CORS allow_origins crashes early if ALLOWED_ORIGINS is unset — no \"*\" fallback (see [SEC:M-1]).\n# @REJECTED Hardcoded allow_origins=[\"*\"] rejected — open CORS allows any origin to access the API,\n# which is a Class 1 security violation in production.\n# @REJECTED SessionMiddleware sharing JWT SECRET_KEY rejected in [SEC:H-4] — key reuse expands blast radius.\n\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\n_session_secret = os.getenv(\"SESSION_SECRET_KEY\", \"\").strip()\nif not _session_secret:\n _session_secret = auth_config.SECRET_KEY\n logger.warning(\n \"SESSION_SECRET_KEY not set. Falling back to AUTH_SECRET_KEY. \"\n \"Set a separate SESSION_SECRET_KEY in .env for production isolation.\"\n )\napp.add_middleware(SessionMiddleware, secret_key=_session_secret)\n\n# Configure CORS\n_allowed_origins_raw = os.getenv(\"ALLOWED_ORIGINS\", \"\").strip()\nif not _allowed_origins_raw:\n logger.warning(\n \"ALLOWED_ORIGINS not set. CORS will reject all cross-origin requests. \"\n \"Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.\"\n )\n _allowed_origins = []\nelse:\n _allowed_origins = [o.strip() for o in _allowed_origins_raw.split(\",\") if o.strip()]\napp.add_middleware(\n CORSMiddleware,\n allow_origins=_allowed_origins,\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# HSTS — Strict-Transport-Security header (see [SEC:L-2])\n# Only active when FORCE_HTTPS=true (enterprise deployments with proper certs).\n# In dev or behind HTTP-only proxies, HSTS is disabled to avoid lockout.\nclass HSTSMiddleware(BaseHTTPMiddleware):\n \"\"\"Add Strict-Transport-Security header when FORCE_HTTPS=true.\n\n Enterprise note: In production, set HSTS at the nginx/ingress level\n (add_header Strict-Transport-Security ...). This middleware is a\n fallback for environments where nginx is not configured to do so.\n \"\"\"\n def __init__(self, app):\n super().__init__(app)\n self._enabled = os.getenv(\"FORCE_HTTPS\", \"\").strip().lower() in {\"1\", \"true\", \"yes\"}\n\n async def dispatch(self, request: Request, call_next):\n response = await call_next(request)\n if self._enabled:\n response.headers[\"Strict-Transport-Security\"] = \"max-age=31536000; includeSubDomains\"\n return response\n\napp.add_middleware(HSTSMiddleware)\n# #endregion app_middleware\n# #region network_error_handler [C:1] [TYPE Function]\n# @BRIEF Global exception handler for NetworkError.\n# @PRE request is a FastAPI Request object.\n# @POST Returns 503 HTTP Exception.\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n logger.error(f\"Network error: {exc}\")\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion network_error_handler\n# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n try:\n response = await call_next(request)\n if not is_polling:\n logger.info(\n f\"Response status: {response.status_code} for {request.url.path}\"\n )\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n# Seed trace_id on every request — MUST be the outermost middleware so trace_id\n# is seeded before log_requests (or any other middleware) runs.\nfrom .core.middleware.trace import TraceContextMiddleware\n\napp.add_middleware(TraceContextMiddleware)\n# #region API_Routes [C:3] [TYPE Block]\n# @BRIEF Register all FastAPI route groups exposed by the application entrypoint.\n# @RELATION DEPENDS_ON -> [FastAPI_App]\n# @RELATION DEPENDS_ON -> [Route_Group_Contracts]\n# @RELATION DEPENDS_ON -> [AuthApi]\n# @RELATION DEPENDS_ON -> [AdminApi]\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]\n# @RELATION DEPENDS_ON -> [ValidationRoutes]\n# @RELATION DEPENDS_ON -> [MaintenanceRouter]\n# Include API routes\napp.include_router(auth.router)\napp.include_router(admin.router)\napp.include_router(admin_api_keys.router)\napp.include_router(plugins.router, prefix=\"/api/plugins\", tags=[\"Plugins\"])\napp.include_router(tasks.router, prefix=\"/api/tasks\", tags=[\"Tasks\"])\napp.include_router(settings.router, prefix=\"/api/settings\", tags=[\"Settings\"])\napp.include_router(environments.router, tags=[\"Environments\"])\napp.include_router(mappings.router, prefix=\"/api/mappings\", tags=[\"Mappings\"])\napp.include_router(migration.router)\napp.include_router(git.router, prefix=\"/api/git\", tags=[\"Git\"])\napp.include_router(llm.router, prefix=\"/api/llm\", tags=[\"LLM\"])\napp.include_router(storage.router, prefix=\"/api/storage\", tags=[\"Storage\"])\napp.include_router(dashboards.router)\napp.include_router(datasets.router)\napp.include_router(reports.router)\napp.include_router(assistant.router, prefix=\"/api/assistant\", tags=[\"Assistant\"])\napp.include_router(clean_release.router)\napp.include_router(clean_release_v2.router)\napp.include_router(profile.router)\napp.include_router(dataset_review.router)\napp.include_router(health.router)\napp.include_router(translate.router)\napp.include_router(validation.router, prefix=\"/api/validation\", tags=[\"Validation\"])\napp.include_router(maintenance.maintenance_router)\n# #endregion API_Routes\n# #region api.include_routers [C:1] [TYPE Action] [SEMANTICS routes, registration, api]\n# @BRIEF Registers all API routers with the FastAPI application.\n# @LAYER API\n# #endregion api.include_routers\n# #region _authenticate_websocket [TYPE Function]\n# @BRIEF Authenticate a WebSocket connection via JWT or API key from query param `token`.\n# @PRE websocket is a live Starlette WebSocket before accept().\n# @POST Returns True if token is valid, logs reason; returns False if rejected.\n# @RELATION DEPENDS_ON -> [AuthJwtModule]\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @SIDE_EFFECT Performs DB read to validate API key hash.\nasync def _authenticate_websocket(websocket: WebSocket, endpoint_name: str) -> bool:\n ws_token = websocket.query_params.get(\"token\", \"\")\n if not ws_token:\n logger.warning(\n \"WebSocket connection rejected: missing token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n\n # Try JWT first, fallback to API key\n try:\n from .core.auth.jwt import decode_token\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n\n # Attempt JWT validation\n payload = decode_token(ws_token)\n user = payload.get(\"sub\")\n if isinstance(user, str) and user:\n logger.reason(\n \"WebSocket authenticated via JWT\",\n extra={\"endpoint\": endpoint_name, \"user\": user},\n )\n return True\n except Exception:\n pass\n\n # Fallback: try API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n from .models.api_key import APIKey\n\n key_hash = hash_api_key(ws_token)\n db = SessionLocal()\n try:\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key and api_key.active:\n logger.reason(\n \"WebSocket authenticated via API key\",\n extra={\"endpoint\": endpoint_name, \"key_name\": api_key.name},\n )\n return True\n finally:\n db.close()\n except Exception:\n pass\n\n logger.warning(\n \"WebSocket connection rejected: invalid token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n# #endregion _authenticate_websocket\n\n# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION CALLS -> [_authenticate_websocket]\n# @PRE task_id must be a valid task ID. WebSocket must be authenticated via `token` query param.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n# @TEST_EDGE ws_auth_missing_token -> connection rejected with 4001\n# @TEST_EDGE ws_auth_invalid_token -> connection rejected with 4001\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n token: JWT or API key for authentication (required, see [SEC:C-3])\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/logs\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n# #region dataset_websocket_endpoint [C:4] [TYPE Function]\n# @BRIEF WebSocket endpoint for dataset.updated events — auto-refresh on task completion.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @PRE env_id must reference a known environment.\n# @POST WebSocket streams dataset.updated events until disconnect.\n# @SIDE_EFFECT Subscribes to dataset event queue in task manager lifecycle.\n@app.websocket(\"/ws/datasets/{env_id}\")\nasync def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):\n seed_trace_id()\n with belief_scope(\"dataset_websocket_endpoint\", f\"env_id={env_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/datasets\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n logger.reason(\"Accepted dataset event WebSocket\", extra={\"env_id\": env_id})\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_dataset_events(env_id)\n try:\n while True:\n event = await queue.get()\n await websocket.send_json(event)\n logger.reflect(\"Forwarded dataset.updated event to client\", extra={\"env_id\": env_id})\n except WebSocketDisconnect:\n logger.reason(\"WebSocket client disconnected from dataset events\", extra={\"env_id\": env_id})\n except Exception as exc:\n logger.explore(\"WebSocket dataset streaming failed\", extra={\"env_id\": env_id, \"error\": str(exc)})\n finally:\n task_manager.unsubscribe_dataset_events(env_id, queue)\n logger.reflect(\"Released dataset event subscription\", extra={\"env_id\": env_id})\n# #endregion dataset_websocket_endpoint\n# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa]\n# @BRIEF Mounts the frontend build directory to serve static assets.\nfrontend_path = project_root / \"frontend\" / \"build\"\nif frontend_path.exists():\n app.mount(\n \"/_app\", StaticFiles(directory=str(frontend_path / \"_app\")), name=\"static\"\n )\n # #region serve_spa [TYPE Function] [C:1]\n # @PURPOSE Serves the SPA frontend for any path not matched by API routes.\n # @PRE frontend_path exists.\n # @POST Returns the requested file or index.html.\n @app.get(\"/{file_path:path}\", include_in_schema=False)\n async def serve_spa(file_path: str):\n with belief_scope(\"serve_spa\"):\n # Only serve SPA for non-API paths\n # API routes are registered separately and should be matched by FastAPI first\n if file_path and (\n file_path.startswith(\"api/\")\n or file_path.startswith(\"/api/\")\n or file_path == \"api\"\n ):\n # This should not happen if API routers are properly registered\n # Return 404 instead of serving HTML\n raise HTTPException(\n status_code=404, detail=f\"API endpoint not found: {file_path}\"\n )\n full_path = frontend_path / file_path\n if file_path and full_path.is_file():\n return FileResponse(str(full_path))\n return FileResponse(str(frontend_path / \"index.html\"))\n # #endregion serve_spa\nelse:\n # #region read_root [TYPE Function] [C:1]\n # @PURPOSE A simple root endpoint to confirm that the API is running when frontend is missing.\n # @PRE None.\n # @POST Returns a JSON message indicating API status.\n @app.get(\"/\")\n async def read_root():\n with belief_scope(\"read_root\"):\n return {\n \"message\": \"Superset Tools API is running (Frontend build not found)\"\n }\n # #endregion read_root\n# #endregion StaticFiles\n# #endregion AppModule\n" + "body": "# #region AppModule [C:5] [TYPE Module] [SEMANTICS fastapi, websocket, startup, scheduler, middleware]\n# @BRIEF The main entry point for the FastAPI application.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @INVARIANT All WebSocket connections must be properly cleaned up on disconnect.\n# @INVARIANT All WebSocket connections must be authenticated via JWT or API key token (see [SEC:C-3]).\n# @PRE Python environment and dependencies installed; configuration database available.\n# @POST FastAPI app instance is created, middleware configured, and routes registered.\n# @SIDE_EFFECT Starts background scheduler and binds network ports for HTTP/WS traffic.\n# @DATA_CONTRACT [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]\n# @RATIONALE Duplicate @RELATION and @INVARIANT lines removed from header. Import sorting unified via ruff isort (I) rule across src/ — 139 fixes applied.\n\nimport asyncio\nfrom contextlib import asynccontextmanager\nimport os\nfrom pathlib import Path\n\n# project_root is used for static files mounting\nproject_root = Path(__file__).resolve().parent.parent.parent\n\nfrom alembic.config import Config as AlembicConfig\nfrom fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect, status\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import FileResponse, JSONResponse\nfrom fastapi.staticfiles import StaticFiles\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.middleware.sessions import SessionMiddleware\n\nfrom alembic import command as alembic_command\n\nfrom .api import auth\nfrom .api.routes import (\n admin,\n admin_api_keys,\n assistant,\n clean_release,\n clean_release_v2,\n dashboards,\n dataset_review,\n datasets,\n environments,\n git,\n health,\n llm,\n maintenance,\n mappings,\n migration,\n plugins,\n profile,\n reports,\n settings,\n storage,\n tasks,\n translate,\n validation,\n)\nfrom .core.auth.security import get_password_hash\nfrom .core.cot_logger import seed_trace_id\nfrom .core.database import AuthSessionLocal, init_db\nfrom .core.encryption_key import ensure_encryption_key\nfrom .core.logger import belief_scope, logger\nfrom .core.utils.network import NetworkError\nfrom .dependencies import get_scheduler_service, get_task_manager\nfrom .models.auth import Role, User\n\n\n# #region lifespan [C:3] [TYPE Function]\n# @BRIEF Async context manager for FastAPI startup/shutdown lifecycle.\n# @RELATION CALLS -> [run_alembic_migrations]\n# @RELATION CALLS -> [init_db]\n# @RELATION CALLS -> [AppDependencies]\n# @POST On startup: schema up-to-date, admin exists, scheduler started. On shutdown: scheduler stopped.\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n # Startup\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n logger.info(\"🔐 Ensuring encryption key...\")\n ensure_encryption_key()\n logger.info(\"📦 Running Alembic migrations...\")\n run_alembic_migrations()\n logger.info(\"🗄️ Initializing database tables...\")\n init_db()\n logger.info(\"👤 Bootstrapping admin user...\")\n ensure_initial_admin_user()\n logger.info(\"⏰ Starting scheduler...\")\n scheduler = get_scheduler_service()\n scheduler.start()\n logger.info(\"✅ Application startup complete\")\n logger.info(\"✅ Application startup complete\")\n yield\n # Shutdown\n scheduler.stop()\n# #endregion lifespan\n# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n lifespan=lifespan,\n)\n# TraceContextMiddleware MUST be the outermost middleware so trace_id\n# is seeded before any other middleware or handler runs.\nfrom .core.middleware.trace import TraceContextMiddleware # noqa: E402\napp.add_middleware(TraceContextMiddleware)\n# #endregion FastAPI_App\n# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> [AuthRepository]\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n # Warn about env-var password — visible via /proc to other processes\n logger.warning(\n \"INITIAL_ADMIN_PASSWORD is set via environment variable. \"\n \"This is visible to other processes on the same host. \"\n \"Consider removing the env var after bootstrap.\"\n )\n db = AuthSessionLocal()\n try:\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n admin_role = Role(name=\"Admin\", description=\"System Administrator\", is_admin=True)\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.info(\n \"Initial admin bootstrap skipped: user '%s' already exists.\", username\n )\n return\n new_user = User(\n username=username,\n email=None,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(\n \"Initial admin user '%s' created from environment bootstrap.\", username\n )\n except Exception as exc:\n db.rollback()\n logger.error(\"Failed to bootstrap initial admin user: %s\", exc)\n raise\n finally:\n db.close()\n# #endregion ensure_initial_admin_user\n# #region run_alembic_migrations [C:2] [TYPE Function]\n# @BRIEF Applies all pending Alembic migrations against DATABASE_URL.\n# @POST All Alembic migrations up to 'head' are applied.\n# @SIDE_EFFECT Executes ALTER TABLE / CREATE TABLE via Alembic chain.\n# @RATIONALE Single source of truth for schema changes. All column additions,\n# drops, renames, and data migrations go through Alembic.\n# init_db() runs after to create any tables that don't have\n# a standalone Alembic migration yet.\ndef run_alembic_migrations() -> None:\n with belief_scope(\"run_alembic_migrations\"):\n try:\n alembic_cfg = AlembicConfig(\"alembic.ini\")\n alembic_cfg.set_main_option(\"script_location\", \"alembic\")\n alembic_command.upgrade(alembic_cfg, \"head\")\n logger.reason(\"Alembic migrations applied up to head\")\n except Exception as exc:\n logger.explore(\n \"Alembic migration failed — check migration chain or alembic.ini\",\n extra={\"error\": str(exc)},\n )\n raise\n\n\n# #endregion run_alembic_migrations\n\n\n\n# #region app_middleware [TYPE Block]\n# @BRIEF Configure application-wide middleware (Session, CORS).\n# @RATIONALE SessionMiddleware uses SESSION_SECRET_KEY independent of JWT SECRET_KEY (see [SEC:H-4]).\n# CORS allow_origins crashes early if ALLOWED_ORIGINS is unset — no \"*\" fallback (see [SEC:M-1]).\n# @REJECTED Hardcoded allow_origins=[\"*\"] rejected — open CORS allows any origin to access the API,\n# which is a Class 1 security violation in production.\n# @REJECTED SessionMiddleware sharing JWT SECRET_KEY rejected in [SEC:H-4] — key reuse expands blast radius.\n\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\n_session_secret = os.getenv(\"SESSION_SECRET_KEY\", \"\").strip()\nif not _session_secret:\n _session_secret = auth_config.SECRET_KEY\n logger.warning(\n \"SESSION_SECRET_KEY not set. Falling back to AUTH_SECRET_KEY. \"\n \"Set a separate SESSION_SECRET_KEY in .env for production isolation.\"\n )\napp.add_middleware(SessionMiddleware, secret_key=_session_secret)\n\n# Configure CORS\n_allowed_origins_raw = os.getenv(\"ALLOWED_ORIGINS\", \"\").strip()\nif not _allowed_origins_raw:\n logger.warning(\n \"ALLOWED_ORIGINS not set. CORS will reject all cross-origin requests. \"\n \"Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.\"\n )\n _allowed_origins = []\nelse:\n _allowed_origins = [o.strip() for o in _allowed_origins_raw.split(\",\") if o.strip()]\napp.add_middleware(\n CORSMiddleware,\n allow_origins=_allowed_origins,\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# HSTS — Strict-Transport-Security header (see [SEC:L-2])\n# Only active when FORCE_HTTPS=true (enterprise deployments with proper certs).\n# In dev or behind HTTP-only proxies, HSTS is disabled to avoid lockout.\nclass HSTSMiddleware(BaseHTTPMiddleware):\n \"\"\"Add Strict-Transport-Security header when FORCE_HTTPS=true.\n\n Enterprise note: In production, set HSTS at the nginx/ingress level\n (add_header Strict-Transport-Security ...). This middleware is a\n fallback for environments where nginx is not configured to do so.\n \"\"\"\n def __init__(self, app):\n super().__init__(app)\n self._enabled = os.getenv(\"FORCE_HTTPS\", \"\").strip().lower() in {\"1\", \"true\", \"yes\"}\n\n async def dispatch(self, request: Request, call_next):\n response = await call_next(request)\n if self._enabled:\n response.headers[\"Strict-Transport-Security\"] = \"max-age=31536000; includeSubDomains\"\n return response\n\napp.add_middleware(HSTSMiddleware)\n# #endregion app_middleware\n# #region global_exception_handler [C:2] [TYPE Function]\n# @BRIEF Global exception handler — logs all unhandled 500 errors into the app logger.\n# @PRE request is a FastAPI Request object.\n# @POST Logs full traceback to superset_tools_app logger; returns 500.\n# @RATIONALE FastAPI/Starlette writes unhandled exceptions to uvicorn.error logger,\n# which is not captured in docker logs. This handler ensures every 500\n# appears in our structured JSON log output.\n@app.exception_handler(Exception)\nasync def global_exception_handler(request: Request, exc: Exception):\n client_host = request.client.host if request.client else \"unknown\"\n logger.exception(\n f\"Unhandled exception: {request.method} {request.url.path}\",\n extra={\n \"src\": \"app.exception_handler\",\n \"path\": request.url.path,\n \"method\": request.method,\n \"query_params\": dict(request.query_params),\n \"client\": client_host,\n },\n )\n return JSONResponse(\n status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n content={\"detail\": \"Internal server error\", \"path\": request.url.path},\n )\n# #endregion global_exception_handler\n\n\n# #region network_error_handler [C:1] [TYPE Function]\n# @BRIEF Global exception handler for NetworkError.\n# @PRE request is a FastAPI Request object.\n# @POST Returns 503 HTTP Exception.\n@app.exception_handler(NetworkError)\nasync def network_error_handler(request: Request, exc: NetworkError):\n with belief_scope(\"network_error_handler\"):\n logger.error(f\"Network error: {exc}\")\n return HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion network_error_handler\n# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n import json as _json, logging as _lg\n if logger.isEnabledFor(_lg.INFO):\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n else:\n _json.dump({\"ts\": __import__('datetime').datetime.now(__import__('datetime').UTC).isoformat(),\n \"level\": \"INFO\", \"src\": \"log_requests\", \"marker\": \"REASON\",\n \"intent\": f\"Incoming request: {request.method} {request.url.path}\"},\n sys.stderr, ensure_ascii=False)\n sys.stderr.write(\"\\n\")\n sys.stderr.flush()\n try:\n response = await call_next(request)\n if not is_polling:\n if logger.isEnabledFor(_lg.INFO):\n logger.info(f\"Response status: {response.status_code} for {request.url.path}\")\n else:\n _json.dump({\"ts\": __import__('datetime').datetime.now(__import__('datetime').UTC).isoformat(),\n \"level\": \"INFO\", \"src\": \"log_requests\", \"marker\": \"REFLECT\",\n \"intent\": f\"Response: {response.status_code} for {request.url.path}\"},\n sys.stderr, ensure_ascii=False)\n sys.stderr.write(\"\\n\")\n sys.stderr.flush()\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n# #region API_Routes [C:3] [TYPE Block]\n# @BRIEF Register all FastAPI route groups exposed by the application entrypoint.\n# @RELATION DEPENDS_ON -> [FastAPI_App]\n# @RELATION DEPENDS_ON -> [Route_Group_Contracts]\n# @RELATION DEPENDS_ON -> [AuthApi]\n# @RELATION DEPENDS_ON -> [AdminApi]\n# @RELATION DEPENDS_ON -> [PluginsRouter]\n# @RELATION DEPENDS_ON -> [TasksRouter]\n# @RELATION DEPENDS_ON -> [SettingsRouter]\n# @RELATION DEPENDS_ON -> [ReportsRouter]\n# @RELATION DEPENDS_ON -> [LlmRoutes]\n# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]\n# @RELATION DEPENDS_ON -> [ValidationRoutes]\n# @RELATION DEPENDS_ON -> [MaintenanceRouter]\n# Include API routes\napp.include_router(auth.router)\napp.include_router(admin.router)\napp.include_router(admin_api_keys.router)\napp.include_router(plugins.router, prefix=\"/api/plugins\", tags=[\"Plugins\"])\napp.include_router(tasks.router, prefix=\"/api/tasks\", tags=[\"Tasks\"])\napp.include_router(settings.router, prefix=\"/api/settings\", tags=[\"Settings\"])\napp.include_router(environments.router, tags=[\"Environments\"])\napp.include_router(mappings.router, prefix=\"/api/mappings\", tags=[\"Mappings\"])\napp.include_router(migration.router)\napp.include_router(git.router, prefix=\"/api/git\", tags=[\"Git\"])\napp.include_router(llm.router, prefix=\"/api/llm\", tags=[\"LLM\"])\napp.include_router(storage.router, prefix=\"/api/storage\", tags=[\"Storage\"])\napp.include_router(dashboards.router)\napp.include_router(datasets.router)\napp.include_router(reports.router)\napp.include_router(assistant.router, prefix=\"/api/assistant\", tags=[\"Assistant\"])\napp.include_router(clean_release.router)\napp.include_router(clean_release_v2.router)\napp.include_router(profile.router)\napp.include_router(dataset_review.router)\napp.include_router(health.router)\napp.include_router(translate.router)\napp.include_router(validation.router, prefix=\"/api/validation\", tags=[\"Validation\"])\napp.include_router(maintenance.maintenance_router)\n# #endregion API_Routes\n# #region api.include_routers [C:1] [TYPE Action] [SEMANTICS routes, registration, api]\n# @BRIEF Registers all API routers with the FastAPI application.\n# @LAYER API\n# #endregion api.include_routers\n# #region _authenticate_websocket [TYPE Function]\n# @BRIEF Authenticate a WebSocket connection via JWT or API key from query param `token`.\n# @PRE websocket is a live Starlette WebSocket before accept().\n# @POST Returns True if token is valid, logs reason; returns False if rejected.\n# @RELATION DEPENDS_ON -> [AuthJwtModule]\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @SIDE_EFFECT Performs DB read to validate API key hash.\nasync def _authenticate_websocket(websocket: WebSocket, endpoint_name: str) -> bool:\n ws_token = websocket.query_params.get(\"token\", \"\")\n if not ws_token:\n logger.warning(\n \"WebSocket connection rejected: missing token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n\n # Try JWT first, fallback to API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.auth.jwt import decode_token\n from .core.database import SessionLocal\n\n # Attempt JWT validation\n payload = decode_token(ws_token)\n user = payload.get(\"sub\")\n if isinstance(user, str) and user:\n logger.reason(\n \"WebSocket authenticated via JWT\",\n extra={\"endpoint\": endpoint_name, \"user\": user},\n )\n return True\n except Exception:\n logger.explore(\"JWT validation failed in WebSocket auth\", error=\"see traceback\")\n pass\n\n # Fallback: try API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n from .models.api_key import APIKey\n\n key_hash = hash_api_key(ws_token)\n db = SessionLocal()\n try:\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key and api_key.active:\n logger.reason(\n \"WebSocket authenticated via API key\",\n extra={\"endpoint\": endpoint_name, \"key_name\": api_key.name},\n )\n return True\n finally:\n db.close()\n except Exception:\n logger.explore(\"API key validation failed in WebSocket auth\", error=\"see traceback\")\n pass\n\n logger.warning(\n \"WebSocket connection rejected: invalid token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n# #endregion _authenticate_websocket\n\n# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION CALLS -> [_authenticate_websocket]\n# @PRE task_id must be a valid task ID. WebSocket must be authenticated via `token` query param.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n# @TEST_EDGE ws_auth_missing_token -> connection rejected with 4001\n# @TEST_EDGE ws_auth_invalid_token -> connection rejected with 4001\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n token: JWT or API key for authentication (required, see [SEC:C-3])\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/logs\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.model_dump()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.model_dump()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n# #region dataset_websocket_endpoint [C:4] [TYPE Function]\n# @BRIEF WebSocket endpoint for dataset.updated events — auto-refresh on task completion.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @PRE env_id must reference a known environment.\n# @POST WebSocket streams dataset.updated events until disconnect.\n# @SIDE_EFFECT Subscribes to dataset event queue in task manager lifecycle.\n@app.websocket(\"/ws/datasets/{env_id}\")\nasync def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):\n seed_trace_id()\n with belief_scope(\"dataset_websocket_endpoint\", f\"env_id={env_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/datasets\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n logger.reason(\"Accepted dataset event WebSocket\", extra={\"env_id\": env_id})\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_dataset_events(env_id)\n try:\n while True:\n event = await queue.get()\n await websocket.send_json(event)\n logger.reflect(\"Forwarded dataset.updated event to client\", extra={\"env_id\": env_id})\n except WebSocketDisconnect:\n logger.reason(\"WebSocket client disconnected from dataset events\", extra={\"env_id\": env_id})\n except Exception as exc:\n logger.explore(\"WebSocket dataset streaming failed\", extra={\"env_id\": env_id, \"error\": str(exc)})\n finally:\n task_manager.unsubscribe_dataset_events(env_id, queue)\n logger.reflect(\"Released dataset event subscription\", extra={\"env_id\": env_id})\n# #endregion dataset_websocket_endpoint\n# #region translate_run_websocket [C:3] [TYPE Function]\n# @BRIEF WebSocket endpoint for translation run progress — streams structured status updates.\n# @PRE run_id must be a valid translation run ID. WebSocket authenticated via `token` query param.\n# @POST Streams run status JSON every second until terminal state or disconnect.\n# @SIDE_EFFECT Queries DB each tick for current run status via TranslationOrchestrator.\n# @UX_STATE Streaming -> Terminal (completed/failed/cancelled) -> Close\n# @UX_FEEDBACK Client receives {status, total_records, successful_records, failed_records, progressPct, ...}\n@app.websocket(\"/ws/translate/run/{run_id}\")\nasync def translate_run_websocket(websocket: WebSocket, run_id: str):\n seed_trace_id()\n if not await _authenticate_websocket(websocket, \"ws/translate/run\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n await websocket.accept()\n logger.reason(\"Accepted translate run WebSocket\", extra={\"run_id\": run_id})\n try:\n while True:\n try:\n from .core.database import SessionLocal\n from .plugins.translate.orchestrator_aggregator import TranslationResultAggregator\n from .core.event_log import EventLog\n db = SessionLocal()\n try:\n event_log = EventLog(db)\n aggregator = TranslationResultAggregator(db, event_log)\n status = aggregator.get_run_status(run_id)\n total = status.get(\"total_records\", 0) or 0\n done = (status.get(\"successful_records\", 0) or 0) + (status.get(\"failed_records\", 0) or 0) + (status.get(\"skipped_records\", 0) or 0)\n progress_pct = round((done / total) * 100) if total > 0 else 0\n status[\"progressPct\"] = progress_pct\n await websocket.send_json(status)\n if status.get(\"status\") in (\"COMPLETED\", \"FAILED\", \"CANCELLED\"):\n await asyncio.sleep(2)\n break\n finally:\n db.close()\n except Exception as tick_err:\n logger.explore(\"Translate run WS tick error\", extra={\"run_id\": run_id, \"error\": str(tick_err)})\n await websocket.send_json({\"error\": str(tick_err)})\n break\n await asyncio.sleep(1)\n except WebSocketDisconnect:\n logger.reason(\"Translate run WS disconnected\", extra={\"run_id\": run_id})\n except Exception as exc:\n logger.explore(\"Translate run WS error\", extra={\"run_id\": run_id, \"error\": str(exc)})\n logger.reflect(\"Translate run WS closed\", extra={\"run_id\": run_id})\n# #endregion translate_run_websocket\n# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa]\n# @BRIEF Mounts the frontend build directory to serve static assets.\nfrontend_path = project_root / \"frontend\" / \"build\"\nif frontend_path.exists():\n app.mount(\n \"/_app\", StaticFiles(directory=str(frontend_path / \"_app\")), name=\"static\"\n )\n # #region serve_spa [TYPE Function] [C:1]\n # @PURPOSE Serves the SPA frontend for any path not matched by API routes.\n # @PRE frontend_path exists.\n # @POST Returns the requested file or index.html.\n @app.get(\"/{file_path:path}\", include_in_schema=False)\n async def serve_spa(file_path: str):\n with belief_scope(\"serve_spa\"):\n # Only serve SPA for non-API paths\n # API routes are registered separately and should be matched by FastAPI first\n if file_path and (\n file_path.startswith(\"api/\")\n or file_path.startswith(\"/api/\")\n or file_path == \"api\"\n ):\n # This should not happen if API routers are properly registered\n # Return 404 instead of serving HTML\n raise HTTPException(\n status_code=404, detail=f\"API endpoint not found: {file_path}\"\n )\n full_path = frontend_path / file_path\n if file_path and full_path.is_file():\n return FileResponse(str(full_path))\n return FileResponse(str(frontend_path / \"index.html\"))\n # #endregion serve_spa\nelse:\n # #region read_root [TYPE Function] [C:1]\n # @PURPOSE A simple root endpoint to confirm that the API is running when frontend is missing.\n # @PRE None.\n # @POST Returns a JSON message indicating API status.\n @app.get(\"/\")\n async def read_root():\n with belief_scope(\"read_root\"):\n return {\n \"message\": \"Superset Tools API is running (Frontend build not found)\"\n }\n # #endregion read_root\n# #endregion StaticFiles\n# #endregion AppModule\n" + }, + { + "contract_id": "lifespan", + "contract_type": "Function", + "file_path": "backend/src/app.py", + "start_line": 67, + "end_line": 94, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Async context manager for FastAPI startup/shutdown lifecycle.", + "COMPLEXITY": 3, + "POST": "On startup: schema up-to-date, admin exists, scheduler started. On shutdown: scheduler stopped." + }, + "relations": [ + { + "source_id": "lifespan", + "relation_type": "CALLS", + "target_id": "run_alembic_migrations", + "target_ref": "[run_alembic_migrations]" + }, + { + "source_id": "lifespan", + "relation_type": "CALLS", + "target_id": "init_db", + "target_ref": "[init_db]" + }, + { + "source_id": "lifespan", + "relation_type": "CALLS", + "target_id": "AppDependencies", + "target_ref": "[AppDependencies]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region lifespan [C:3] [TYPE Function]\n# @BRIEF Async context manager for FastAPI startup/shutdown lifecycle.\n# @RELATION CALLS -> [run_alembic_migrations]\n# @RELATION CALLS -> [init_db]\n# @RELATION CALLS -> [AppDependencies]\n# @POST On startup: schema up-to-date, admin exists, scheduler started. On shutdown: scheduler stopped.\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n # Startup\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n logger.info(\"🔐 Ensuring encryption key...\")\n ensure_encryption_key()\n logger.info(\"📦 Running Alembic migrations...\")\n run_alembic_migrations()\n logger.info(\"🗄️ Initializing database tables...\")\n init_db()\n logger.info(\"👤 Bootstrapping admin user...\")\n ensure_initial_admin_user()\n logger.info(\"⏰ Starting scheduler...\")\n scheduler = get_scheduler_service()\n scheduler.start()\n logger.info(\"✅ Application startup complete\")\n logger.info(\"✅ Application startup complete\")\n yield\n # Shutdown\n scheduler.stop()\n# #endregion lifespan\n" }, { "contract_id": "FastAPI_App", "contract_type": "Global", "file_path": "backend/src/app.py", - "start_line": 62, - "end_line": 71, + "start_line": 95, + "end_line": 109, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -16331,14 +17218,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n)\n# #endregion FastAPI_App\n" + "body": "# #region FastAPI_App [C:3] [TYPE Global] [SEMANTICS app, fastapi, instance, route-registry]\n# @BRIEF Canonical FastAPI application instance for route, middleware, and websocket registration.\n# @RELATION DEPENDS_ON -> [ApiRoutesModule]\n# @RELATION BINDS_TO -> [API_Routes]\napp = FastAPI(\n title=\"Superset Tools API\",\n description=\"API for managing Superset automation tools and plugins.\",\n version=\"1.0.0\",\n lifespan=lifespan,\n)\n# TraceContextMiddleware MUST be the outermost middleware so trace_id\n# is seeded before any other middleware or handler runs.\nfrom .core.middleware.trace import TraceContextMiddleware # noqa: E402\napp.add_middleware(TraceContextMiddleware)\n# #endregion FastAPI_App\n" }, { "contract_id": "ensure_initial_admin_user", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 72, - "end_line": 125, + "start_line": 110, + "end_line": 163, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -16359,65 +17246,32 @@ "body": "# #region ensure_initial_admin_user [C:3] [TYPE Function]\n# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.\n# @RELATION DEPENDS_ON -> [AuthRepository]\ndef ensure_initial_admin_user() -> None:\n raw_flag = os.getenv(\"INITIAL_ADMIN_CREATE\", \"false\").strip().lower()\n if raw_flag not in {\"1\", \"true\", \"yes\", \"on\"}:\n return\n username = os.getenv(\"INITIAL_ADMIN_USERNAME\", \"\").strip()\n password = os.getenv(\"INITIAL_ADMIN_PASSWORD\", \"\").strip()\n if not username or not password:\n logger.warning(\n \"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap.\"\n )\n return\n # Warn about env-var password — visible via /proc to other processes\n logger.warning(\n \"INITIAL_ADMIN_PASSWORD is set via environment variable. \"\n \"This is visible to other processes on the same host. \"\n \"Consider removing the env var after bootstrap.\"\n )\n db = AuthSessionLocal()\n try:\n admin_role = db.query(Role).filter(Role.name == \"Admin\").first()\n if not admin_role:\n admin_role = Role(name=\"Admin\", description=\"System Administrator\", is_admin=True)\n db.add(admin_role)\n db.commit()\n db.refresh(admin_role)\n existing_user = db.query(User).filter(User.username == username).first()\n if existing_user:\n logger.info(\n \"Initial admin bootstrap skipped: user '%s' already exists.\", username\n )\n return\n new_user = User(\n username=username,\n email=None,\n password_hash=get_password_hash(password),\n auth_source=\"LOCAL\",\n is_active=True,\n )\n new_user.roles.append(admin_role)\n db.add(new_user)\n db.commit()\n logger.info(\n \"Initial admin user '%s' created from environment bootstrap.\", username\n )\n except Exception as exc:\n db.rollback()\n logger.error(\"Failed to bootstrap initial admin user: %s\", exc)\n raise\n finally:\n db.close()\n# #endregion ensure_initial_admin_user\n" }, { - "contract_id": "startup_event", + "contract_id": "run_alembic_migrations", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 126, - "end_line": 141, - "tier": "TIER_2", - "complexity": 3, + "start_line": 164, + "end_line": 187, + "tier": "TIER_1", + "complexity": 2, "metadata": { - "BRIEF": "Handles application startup tasks, such as starting the scheduler.", - "COMPLEXITY": 3, - "POST": "Scheduler is started.", - "PRE": "None." + "BRIEF": "Applies all pending Alembic migrations against DATABASE_URL.", + "COMPLEXITY": 2, + "POST": "All Alembic migrations up to 'head' are applied.", + "RATIONALE": "Single source of truth for schema changes. All column additions,", + "SIDE_EFFECT": "Executes ALTER TABLE / CREATE TABLE via Alembic chain." }, - "relations": [ - { - "source_id": "startup_event", - "relation_type": "CALLS", - "target_id": "AppDependencies", - "target_ref": "[AppDependencies]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region startup_event [C:3] [TYPE Function]\n# @BRIEF Handles application startup tasks, such as starting the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is started.\n# Startup event\n@app.on_event(\"startup\")\nasync def startup_event():\n seed_trace_id()\n with belief_scope(\"startup_event\"):\n ensure_encryption_key()\n init_db()\n ensure_initial_admin_user()\n scheduler = get_scheduler_service()\n scheduler.start()\n# #endregion startup_event\n" - }, - { - "contract_id": "shutdown_event", - "contract_type": "Function", - "file_path": "backend/src/app.py", - "start_line": 142, - "end_line": 154, - "tier": "TIER_2", - "complexity": 3, - "metadata": { - "BRIEF": "Handles application shutdown tasks, such as stopping the scheduler.", - "COMPLEXITY": 3, - "POST": "Scheduler is stopped.", - "PRE": "None." - }, - "relations": [ - { - "source_id": "shutdown_event", - "relation_type": "CALLS", - "target_id": "AppDependencies", - "target_ref": "[AppDependencies]" - } - ], - "schema_warnings": [], - "anchor_syntax": "region", - "tier_source": "AutoCalculated", - "body": "# #region shutdown_event [C:3] [TYPE Function]\n# @BRIEF Handles application shutdown tasks, such as stopping the scheduler.\n# @RELATION CALLS -> [AppDependencies]\n# @PRE None.\n# @POST Scheduler is stopped.\n# Shutdown event\n@app.on_event(\"shutdown\")\nasync def shutdown_event():\n seed_trace_id()\n with belief_scope(\"shutdown_event\"):\n scheduler = get_scheduler_service()\n scheduler.stop()\n# #endregion shutdown_event\n" + "body": "# #region run_alembic_migrations [C:2] [TYPE Function]\n# @BRIEF Applies all pending Alembic migrations against DATABASE_URL.\n# @POST All Alembic migrations up to 'head' are applied.\n# @SIDE_EFFECT Executes ALTER TABLE / CREATE TABLE via Alembic chain.\n# @RATIONALE Single source of truth for schema changes. All column additions,\n# drops, renames, and data migrations go through Alembic.\n# init_db() runs after to create any tables that don't have\n# a standalone Alembic migration yet.\ndef run_alembic_migrations() -> None:\n with belief_scope(\"run_alembic_migrations\"):\n try:\n alembic_cfg = AlembicConfig(\"alembic.ini\")\n alembic_cfg.set_main_option(\"script_location\", \"alembic\")\n alembic_command.upgrade(alembic_cfg, \"head\")\n logger.reason(\"Alembic migrations applied up to head\")\n except Exception as exc:\n logger.explore(\n \"Alembic migration failed — check migration chain or alembic.ini\",\n extra={\"error\": str(exc)},\n )\n raise\n\n\n# #endregion run_alembic_migrations\n" }, { "contract_id": "app_middleware", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 155, - "end_line": 214, + "start_line": 191, + "end_line": 250, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16430,12 +17284,33 @@ "tier_source": "AutoCalculated", "body": "# #region app_middleware [TYPE Block]\n# @BRIEF Configure application-wide middleware (Session, CORS).\n# @RATIONALE SessionMiddleware uses SESSION_SECRET_KEY independent of JWT SECRET_KEY (see [SEC:H-4]).\n# CORS allow_origins crashes early if ALLOWED_ORIGINS is unset — no \"*\" fallback (see [SEC:M-1]).\n# @REJECTED Hardcoded allow_origins=[\"*\"] rejected — open CORS allows any origin to access the API,\n# which is a Class 1 security violation in production.\n# @REJECTED SessionMiddleware sharing JWT SECRET_KEY rejected in [SEC:H-4] — key reuse expands blast radius.\n\n# Configure Session Middleware (required by Authlib for OAuth2 flow)\nfrom .core.auth.config import auth_config\n\n_session_secret = os.getenv(\"SESSION_SECRET_KEY\", \"\").strip()\nif not _session_secret:\n _session_secret = auth_config.SECRET_KEY\n logger.warning(\n \"SESSION_SECRET_KEY not set. Falling back to AUTH_SECRET_KEY. \"\n \"Set a separate SESSION_SECRET_KEY in .env for production isolation.\"\n )\napp.add_middleware(SessionMiddleware, secret_key=_session_secret)\n\n# Configure CORS\n_allowed_origins_raw = os.getenv(\"ALLOWED_ORIGINS\", \"\").strip()\nif not _allowed_origins_raw:\n logger.warning(\n \"ALLOWED_ORIGINS not set. CORS will reject all cross-origin requests. \"\n \"Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.\"\n )\n _allowed_origins = []\nelse:\n _allowed_origins = [o.strip() for o in _allowed_origins_raw.split(\",\") if o.strip()]\napp.add_middleware(\n CORSMiddleware,\n allow_origins=_allowed_origins,\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n)\n\n# HSTS — Strict-Transport-Security header (see [SEC:L-2])\n# Only active when FORCE_HTTPS=true (enterprise deployments with proper certs).\n# In dev or behind HTTP-only proxies, HSTS is disabled to avoid lockout.\nclass HSTSMiddleware(BaseHTTPMiddleware):\n \"\"\"Add Strict-Transport-Security header when FORCE_HTTPS=true.\n\n Enterprise note: In production, set HSTS at the nginx/ingress level\n (add_header Strict-Transport-Security ...). This middleware is a\n fallback for environments where nginx is not configured to do so.\n \"\"\"\n def __init__(self, app):\n super().__init__(app)\n self._enabled = os.getenv(\"FORCE_HTTPS\", \"\").strip().lower() in {\"1\", \"true\", \"yes\"}\n\n async def dispatch(self, request: Request, call_next):\n response = await call_next(request)\n if self._enabled:\n response.headers[\"Strict-Transport-Security\"] = \"max-age=31536000; includeSubDomains\"\n return response\n\napp.add_middleware(HSTSMiddleware)\n# #endregion app_middleware\n" }, + { + "contract_id": "global_exception_handler", + "contract_type": "Function", + "file_path": "backend/src/app.py", + "start_line": 251, + "end_line": 275, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Global exception handler — logs all unhandled 500 errors into the app logger.", + "COMPLEXITY": 2, + "POST": "Logs full traceback to superset_tools_app logger; returns 500.", + "PRE": "request is a FastAPI Request object.", + "RATIONALE": "FastAPI/Starlette writes unhandled exceptions to uvicorn.error logger," + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region global_exception_handler [C:2] [TYPE Function]\n# @BRIEF Global exception handler — logs all unhandled 500 errors into the app logger.\n# @PRE request is a FastAPI Request object.\n# @POST Logs full traceback to superset_tools_app logger; returns 500.\n# @RATIONALE FastAPI/Starlette writes unhandled exceptions to uvicorn.error logger,\n# which is not captured in docker logs. This handler ensures every 500\n# appears in our structured JSON log output.\n@app.exception_handler(Exception)\nasync def global_exception_handler(request: Request, exc: Exception):\n client_host = request.client.host if request.client else \"unknown\"\n logger.exception(\n f\"Unhandled exception: {request.method} {request.url.path}\",\n extra={\n \"src\": \"app.exception_handler\",\n \"path\": request.url.path,\n \"method\": request.method,\n \"query_params\": dict(request.query_params),\n \"client\": client_host,\n },\n )\n return JSONResponse(\n status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n content={\"detail\": \"Internal server error\", \"path\": request.url.path},\n )\n# #endregion global_exception_handler\n" + }, { "contract_id": "network_error_handler", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 215, - "end_line": 227, + "start_line": 278, + "end_line": 290, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16454,8 +17329,8 @@ "contract_id": "log_requests", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 228, - "end_line": 253, + "start_line": 291, + "end_line": 331, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -16475,14 +17350,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n try:\n response = await call_next(request)\n if not is_polling:\n logger.info(\n f\"Response status: {response.status_code} for {request.url.path}\"\n )\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n" + "body": "# #region log_requests [C:3] [TYPE Function]\n# @BRIEF Middleware to log incoming HTTP requests and their response status.\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @PRE request is a FastAPI Request object.\n# @POST Logs request and response details.\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n with belief_scope(\"log_requests\"):\n # Avoid spamming logs for polling endpoints\n is_polling = request.url.path.endswith(\"/api/tasks\") and request.method == \"GET\"\n if not is_polling:\n import json as _json, logging as _lg\n if logger.isEnabledFor(_lg.INFO):\n logger.info(f\"Incoming request: {request.method} {request.url.path}\")\n else:\n _json.dump({\"ts\": __import__('datetime').datetime.now(__import__('datetime').UTC).isoformat(),\n \"level\": \"INFO\", \"src\": \"log_requests\", \"marker\": \"REASON\",\n \"intent\": f\"Incoming request: {request.method} {request.url.path}\"},\n sys.stderr, ensure_ascii=False)\n sys.stderr.write(\"\\n\")\n sys.stderr.flush()\n try:\n response = await call_next(request)\n if not is_polling:\n if logger.isEnabledFor(_lg.INFO):\n logger.info(f\"Response status: {response.status_code} for {request.url.path}\")\n else:\n _json.dump({\"ts\": __import__('datetime').datetime.now(__import__('datetime').UTC).isoformat(),\n \"level\": \"INFO\", \"src\": \"log_requests\", \"marker\": \"REFLECT\",\n \"intent\": f\"Response: {response.status_code} for {request.url.path}\"},\n sys.stderr, ensure_ascii=False)\n sys.stderr.write(\"\\n\")\n sys.stderr.flush()\n return response\n except NetworkError as e:\n logger.error(f\"Network error caught in middleware: {e}\")\n raise HTTPException(\n status_code=503,\n detail=\"Environment unavailable. Please check if the Superset instance is running.\",\n )\n# #endregion log_requests\n" }, { "contract_id": "API_Routes", "contract_type": "Block", "file_path": "backend/src/app.py", - "start_line": 259, - "end_line": 298, + "start_line": 332, + "end_line": 371, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -16572,8 +17447,8 @@ "contract_id": "api.include_routers", "contract_type": "Action", "file_path": "backend/src/app.py", - "start_line": 299, - "end_line": 302, + "start_line": 372, + "end_line": 375, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16591,8 +17466,8 @@ "contract_id": "_authenticate_websocket", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 303, - "end_line": 363, + "start_line": 376, + "end_line": 438, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16618,14 +17493,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _authenticate_websocket [TYPE Function]\n# @BRIEF Authenticate a WebSocket connection via JWT or API key from query param `token`.\n# @PRE websocket is a live Starlette WebSocket before accept().\n# @POST Returns True if token is valid, logs reason; returns False if rejected.\n# @RELATION DEPENDS_ON -> [AuthJwtModule]\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @SIDE_EFFECT Performs DB read to validate API key hash.\nasync def _authenticate_websocket(websocket: WebSocket, endpoint_name: str) -> bool:\n ws_token = websocket.query_params.get(\"token\", \"\")\n if not ws_token:\n logger.warning(\n \"WebSocket connection rejected: missing token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n\n # Try JWT first, fallback to API key\n try:\n from .core.auth.jwt import decode_token\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n\n # Attempt JWT validation\n payload = decode_token(ws_token)\n user = payload.get(\"sub\")\n if isinstance(user, str) and user:\n logger.reason(\n \"WebSocket authenticated via JWT\",\n extra={\"endpoint\": endpoint_name, \"user\": user},\n )\n return True\n except Exception:\n pass\n\n # Fallback: try API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n from .models.api_key import APIKey\n\n key_hash = hash_api_key(ws_token)\n db = SessionLocal()\n try:\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key and api_key.active:\n logger.reason(\n \"WebSocket authenticated via API key\",\n extra={\"endpoint\": endpoint_name, \"key_name\": api_key.name},\n )\n return True\n finally:\n db.close()\n except Exception:\n pass\n\n logger.warning(\n \"WebSocket connection rejected: invalid token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n# #endregion _authenticate_websocket\n" + "body": "# #region _authenticate_websocket [TYPE Function]\n# @BRIEF Authenticate a WebSocket connection via JWT or API key from query param `token`.\n# @PRE websocket is a live Starlette WebSocket before accept().\n# @POST Returns True if token is valid, logs reason; returns False if rejected.\n# @RELATION DEPENDS_ON -> [AuthJwtModule]\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @SIDE_EFFECT Performs DB read to validate API key hash.\nasync def _authenticate_websocket(websocket: WebSocket, endpoint_name: str) -> bool:\n ws_token = websocket.query_params.get(\"token\", \"\")\n if not ws_token:\n logger.warning(\n \"WebSocket connection rejected: missing token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n\n # Try JWT first, fallback to API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.auth.jwt import decode_token\n from .core.database import SessionLocal\n\n # Attempt JWT validation\n payload = decode_token(ws_token)\n user = payload.get(\"sub\")\n if isinstance(user, str) and user:\n logger.reason(\n \"WebSocket authenticated via JWT\",\n extra={\"endpoint\": endpoint_name, \"user\": user},\n )\n return True\n except Exception:\n logger.explore(\"JWT validation failed in WebSocket auth\", error=\"see traceback\")\n pass\n\n # Fallback: try API key\n try:\n from .core.auth.api_key import hash_api_key\n from .core.database import SessionLocal\n from .models.api_key import APIKey\n\n key_hash = hash_api_key(ws_token)\n db = SessionLocal()\n try:\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key and api_key.active:\n logger.reason(\n \"WebSocket authenticated via API key\",\n extra={\"endpoint\": endpoint_name, \"key_name\": api_key.name},\n )\n return True\n finally:\n db.close()\n except Exception:\n logger.explore(\"API key validation failed in WebSocket auth\", error=\"see traceback\")\n pass\n\n logger.warning(\n \"WebSocket connection rejected: invalid token\",\n extra={\"endpoint\": endpoint_name},\n )\n return False\n# #endregion _authenticate_websocket\n" }, { "contract_id": "websocket_endpoint", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 365, - "end_line": 518, + "start_line": 440, + "end_line": 593, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -16662,14 +17537,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION CALLS -> [_authenticate_websocket]\n# @PRE task_id must be a valid task ID. WebSocket must be authenticated via `token` query param.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n# @TEST_EDGE ws_auth_missing_token -> connection rejected with 4001\n# @TEST_EDGE ws_auth_invalid_token -> connection rejected with 4001\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n token: JWT or API key for authentication (required, see [SEC:C-3])\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/logs\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.dict()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n" + "body": "# #region websocket_endpoint [C:5] [TYPE Function]\n# @BRIEF Provides a WebSocket endpoint for real-time log streaming of a task with server-side filtering.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [LoggerModule]\n# @RELATION CALLS -> [_authenticate_websocket]\n# @PRE task_id must be a valid task ID. WebSocket must be authenticated via `token` query param.\n# @POST WebSocket connection is managed and logs are streamed until disconnect.\n# @SIDE_EFFECT Subscribes to TaskManager log queue and broadcasts messages over network.\n# @DATA_CONTRACT [task_id: str, source: str, level: str] -> [JSON log entry objects]\n# @INVARIANT Every accepted WebSocket subscription is unsubscribed exactly once even when streaming fails or the client disconnects.\n# @UX_STATE Connecting -> Streaming -> (Disconnected)\n#\n# @TEST_CONTRACT WebSocketLogStreamApi ->\n# {\n# required_fields: {websocket: WebSocket, task_id: str},\n# optional_fields: {source: str, level: str},\n# invariants: [\n# \"Accepts the WebSocket connection\",\n# \"Applies source and level filters correctly to streamed logs\",\n# \"Cleans up subscriptions on disconnect\"\n# ]\n# }\n# @TEST_FIXTURE valid_ws_connection -> {\"task_id\": \"test_1\", \"source\": \"plugin\"}\n# @TEST_EDGE task_not_found_ws -> closes connection or sends error\n# @TEST_EDGE empty_task_logs -> waits for new logs\n# @TEST_INVARIANT consistent_streaming -> verifies: [valid_ws_connection]\n# @TEST_EDGE ws_auth_missing_token -> connection rejected with 4001\n# @TEST_EDGE ws_auth_invalid_token -> connection rejected with 4001\n@app.websocket(\"/ws/logs/{task_id}\")\nasync def websocket_endpoint(\n websocket: WebSocket, task_id: str, source: str = None, level: str = None\n):\n \"\"\"\n WebSocket endpoint for real-time log streaming with optional server-side filtering.\n Query Parameters:\n source: Filter logs by source component (e.g., \"plugin\", \"superset_api\")\n level: Filter logs by minimum level (DEBUG, INFO, WARNING, ERROR)\n token: JWT or API key for authentication (required, see [SEC:C-3])\n \"\"\"\n seed_trace_id()\n with belief_scope(\"websocket_endpoint\", f\"task_id={task_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/logs\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n source_filter = source.lower() if source else None\n level_filter = level.upper() if level else None\n level_hierarchy = {\"DEBUG\": 0, \"INFO\": 1, \"WARNING\": 2, \"ERROR\": 3}\n min_level = level_hierarchy.get(level_filter, 0) if level_filter else 0\n logger.reason(\n \"Accepted WebSocket log stream connection\",\n extra={\n \"task_id\": task_id,\n \"source_filter\": source_filter,\n \"level_filter\": level_filter,\n \"min_level\": min_level,\n },\n )\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_logs(task_id)\n logger.reason(\n \"Subscribed WebSocket client to task log queue\",\n extra={\"task_id\": task_id},\n )\n def matches_filters(log_entry) -> bool:\n \"\"\"Check if log entry matches the filter criteria.\"\"\"\n log_source = getattr(log_entry, \"source\", None)\n if source_filter and str(log_source or \"\").lower() != source_filter:\n return False\n if level_filter:\n log_level = level_hierarchy.get(str(log_entry.level).upper(), 0)\n if log_level < min_level:\n return False\n return True\n try:\n logger.reason(\n \"Starting task log stream replay and live forwarding\",\n extra={\"task_id\": task_id},\n )\n initial_logs = task_manager.get_task_logs(task_id)\n initial_sent = 0\n for log_entry in initial_logs:\n if matches_filters(log_entry):\n log_dict = log_entry.model_dump()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n initial_sent += 1\n logger.reflect(\n \"Initial task log replay completed\",\n extra={\n \"task_id\": task_id,\n \"replayed_logs\": initial_sent,\n \"total_available_logs\": len(initial_logs),\n },\n )\n task = task_manager.get_task(task_id)\n if task and task.status == \"AWAITING_INPUT\" and task.input_request:\n synthetic_log = {\n \"timestamp\": task.logs[-1].timestamp.isoformat()\n if task.logs\n else \"2024-01-01T00:00:00\",\n \"level\": \"INFO\",\n \"message\": \"Task paused for user input (Connection Re-established)\",\n \"context\": {\"input_request\": task.input_request},\n }\n await websocket.send_json(synthetic_log)\n logger.reason(\n \"Replayed awaiting-input prompt to restored WebSocket client\",\n extra={\"task_id\": task_id, \"task_status\": task.status},\n )\n while True:\n log_entry = await queue.get()\n if not matches_filters(log_entry):\n continue\n log_dict = log_entry.model_dump()\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n await websocket.send_json(log_dict)\n logger.reflect(\n \"Forwarded task log entry to WebSocket client\",\n extra={\n \"task_id\": task_id,\n \"level\": log_dict.get(\"level\"),\n },\n )\n if (\n \"Task completed successfully\" in log_entry.message\n or \"Task failed\" in log_entry.message\n ):\n logger.reason(\n \"Observed terminal task log entry; delaying to preserve client visibility\",\n extra={\"task_id\": task_id, \"message\": log_entry.message},\n )\n await asyncio.sleep(2)\n except WebSocketDisconnect:\n logger.reason(\n \"WebSocket client disconnected from task log stream\",\n extra={\"task_id\": task_id},\n )\n except Exception as exc:\n logger.explore(\n \"WebSocket log streaming encountered an unexpected failure\",\n extra={\"task_id\": task_id, \"error\": str(exc)},\n )\n raise\n finally:\n task_manager.unsubscribe_logs(task_id, queue)\n logger.reflect(\n \"Released WebSocket log queue subscription\",\n extra={\"task_id\": task_id},\n )\n# #endregion websocket_endpoint\n" }, { "contract_id": "dataset_websocket_endpoint", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 519, - "end_line": 551, + "start_line": 594, + "end_line": 626, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -16692,12 +17567,35 @@ "tier_source": "AutoCalculated", "body": "# #region dataset_websocket_endpoint [C:4] [TYPE Function]\n# @BRIEF WebSocket endpoint for dataset.updated events — auto-refresh on task completion.\n# @RELATION CALLS -> [TaskManagerPackage]\n# @PRE env_id must reference a known environment.\n# @POST WebSocket streams dataset.updated events until disconnect.\n# @SIDE_EFFECT Subscribes to dataset event queue in task manager lifecycle.\n@app.websocket(\"/ws/datasets/{env_id}\")\nasync def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):\n seed_trace_id()\n with belief_scope(\"dataset_websocket_endpoint\", f\"env_id={env_id}\"):\n\n # ── WebSocket authentication (see [SEC:C-3]) ──\n if not await _authenticate_websocket(websocket, \"ws/datasets\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n\n await websocket.accept()\n logger.reason(\"Accepted dataset event WebSocket\", extra={\"env_id\": env_id})\n task_manager = get_task_manager()\n queue = await task_manager.subscribe_dataset_events(env_id)\n try:\n while True:\n event = await queue.get()\n await websocket.send_json(event)\n logger.reflect(\"Forwarded dataset.updated event to client\", extra={\"env_id\": env_id})\n except WebSocketDisconnect:\n logger.reason(\"WebSocket client disconnected from dataset events\", extra={\"env_id\": env_id})\n except Exception as exc:\n logger.explore(\"WebSocket dataset streaming failed\", extra={\"env_id\": env_id, \"error\": str(exc)})\n finally:\n task_manager.unsubscribe_dataset_events(env_id, queue)\n logger.reflect(\"Released dataset event subscription\", extra={\"env_id\": env_id})\n# #endregion dataset_websocket_endpoint\n" }, + { + "contract_id": "translate_run_websocket", + "contract_type": "Function", + "file_path": "backend/src/app.py", + "start_line": 627, + "end_line": 673, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "WebSocket endpoint for translation run progress — streams structured status updates.", + "COMPLEXITY": 3, + "POST": "Streams run status JSON every second until terminal state or disconnect.", + "PRE": "run_id must be a valid translation run ID. WebSocket authenticated via `token` query param.", + "SIDE_EFFECT": "Queries DB each tick for current run status via TranslationOrchestrator.", + "UX_FEEDBACK": "Client receives {status, total_records, successful_records, failed_records, progressPct, ...}", + "UX_STATE": "Streaming -> Terminal (completed/failed/cancelled) -> Close" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region translate_run_websocket [C:3] [TYPE Function]\n# @BRIEF WebSocket endpoint for translation run progress — streams structured status updates.\n# @PRE run_id must be a valid translation run ID. WebSocket authenticated via `token` query param.\n# @POST Streams run status JSON every second until terminal state or disconnect.\n# @SIDE_EFFECT Queries DB each tick for current run status via TranslationOrchestrator.\n# @UX_STATE Streaming -> Terminal (completed/failed/cancelled) -> Close\n# @UX_FEEDBACK Client receives {status, total_records, successful_records, failed_records, progressPct, ...}\n@app.websocket(\"/ws/translate/run/{run_id}\")\nasync def translate_run_websocket(websocket: WebSocket, run_id: str):\n seed_trace_id()\n if not await _authenticate_websocket(websocket, \"ws/translate/run\"):\n await websocket.close(code=4001, reason=\"Authentication required\")\n return\n await websocket.accept()\n logger.reason(\"Accepted translate run WebSocket\", extra={\"run_id\": run_id})\n try:\n while True:\n try:\n from .core.database import SessionLocal\n from .plugins.translate.orchestrator_aggregator import TranslationResultAggregator\n from .core.event_log import EventLog\n db = SessionLocal()\n try:\n event_log = EventLog(db)\n aggregator = TranslationResultAggregator(db, event_log)\n status = aggregator.get_run_status(run_id)\n total = status.get(\"total_records\", 0) or 0\n done = (status.get(\"successful_records\", 0) or 0) + (status.get(\"failed_records\", 0) or 0) + (status.get(\"skipped_records\", 0) or 0)\n progress_pct = round((done / total) * 100) if total > 0 else 0\n status[\"progressPct\"] = progress_pct\n await websocket.send_json(status)\n if status.get(\"status\") in (\"COMPLETED\", \"FAILED\", \"CANCELLED\"):\n await asyncio.sleep(2)\n break\n finally:\n db.close()\n except Exception as tick_err:\n logger.explore(\"Translate run WS tick error\", extra={\"run_id\": run_id, \"error\": str(tick_err)})\n await websocket.send_json({\"error\": str(tick_err)})\n break\n await asyncio.sleep(1)\n except WebSocketDisconnect:\n logger.reason(\"Translate run WS disconnected\", extra={\"run_id\": run_id})\n except Exception as exc:\n logger.explore(\"Translate run WS error\", extra={\"run_id\": run_id, \"error\": str(exc)})\n logger.reflect(\"Translate run WS closed\", extra={\"run_id\": run_id})\n# #endregion translate_run_websocket\n" + }, { "contract_id": "StaticFiles", "contract_type": "Mount", "file_path": "backend/src/app.py", - "start_line": 552, - "end_line": 595, + "start_line": 674, + "end_line": 717, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16714,8 +17612,8 @@ "contract_id": "serve_spa", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 559, - "end_line": 582, + "start_line": 681, + "end_line": 704, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -16734,8 +17632,8 @@ "contract_id": "read_root", "contract_type": "Function", "file_path": "backend/src/app.py", - "start_line": 584, - "end_line": 594, + "start_line": 706, + "end_line": 716, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19017,7 +19915,7 @@ "contract_type": "Module", "file_path": "backend/src/core/auth/jwt.py", "start_line": 1, - "end_line": 173, + "end_line": 172, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -19043,14 +19941,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token]\n#\n# @BRIEF JWT token generation and validation logic with server-side revocation.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\n#\n# @INVARIANT Tokens must include aud, iss, iat, jti, exp, and sub claims.\n# Revoked tokens are rejected even if not expired.\n# @PRE JWT secret configured in environment\n# @POST Token encode/decode/revoke functions exported\n# @SIDE_EFFECT None\n# @DATA_CONTRACT TokenPayload -> JWT string\n\nimport hashlib\nimport uuid\nfrom datetime import datetime, timedelta, timezone\n\nfrom jose import jwt\nfrom jose.exceptions import JWTError\nfrom sqlalchemy.orm import Session\n\nfrom ...models.auth import TokenBlacklist\nfrom ..logger import belief_scope\nfrom .config import auth_config\n\n\n# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token with aud, iss, iat, jti, exp claims.\n# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST Returns a signed JWT string with unique jti for revocation.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n now = datetime.now(timezone.utc)\n if expires_delta:\n expire = now + expires_delta\n else:\n expire = now + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\n \"exp\": expire,\n \"iat\": now,\n \"jti\": str(uuid.uuid4()),\n \"aud\": auth_config.JWT_AUDIENCE,\n \"iss\": auth_config.JWT_ISSUER,\n })\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n\n\n# #region decode_token [TYPE Function]\n# @BRIEF Decodes and validates a JWT token — verifies aud, iss, exp, and blacklist.\n# @PRE token is a signed JWT string.\n# @POST Returns the decoded payload if valid. Raises JWTError if invalid.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef decode_token(token: str) -> dict:\n with belief_scope(\"decode_token\"):\n payload = jwt.decode(\n token,\n auth_config.SECRET_KEY,\n algorithms=[auth_config.ALGORITHM],\n audience=auth_config.JWT_AUDIENCE,\n issuer=auth_config.JWT_ISSUER,\n options={\n \"verify_aud\": True,\n \"verify_exp\": True,\n \"verify_iat\": True,\n \"require\": [\"exp\", \"sub\", \"aud\", \"iss\", \"iat\", \"jti\"],\n },\n )\n return payload\n\n\n# #endregion decode_token\n\n\n# #region _hash_token [TYPE Function]\n# @BRIEF SHA-256 hash of a token string for blacklist lookup.\n# @PRE token is a non-empty JWT string.\n# @POST Returns 64-character hex digest.\ndef _hash_token(token: str) -> str:\n return hashlib.sha256(token.encode()).hexdigest()\n\n\n# #endregion _hash_token\n\n\n# #region _prune_blacklist [TYPE Function]\n# @BRIEF Delete expired entries from the token blacklist.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Expired blacklist rows are removed.\ndef _prune_blacklist(db: Session) -> None:\n now = datetime.now(timezone.utc)\n db.query(TokenBlacklist).filter(TokenBlacklist.expires_at < now).delete()\n db.commit()\n\n\n# #endregion _prune_blacklist\n\n\n# #region blacklist_token [TYPE Function]\n# @BRIEF Revoke a JWT token by adding its hash to the blacklist.\n# @PRE token was issued by this service and hasn't expired.\n# @POST Token is blacklisted and subsequent decode_token calls will reject it.\n# @SIDE_EFFECT Writes to token_blacklist table; prunes expired entries.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef blacklist_token(token: str, db: Session) -> None:\n with belief_scope(\"blacklist_token\"):\n _prune_blacklist(db)\n try:\n payload = decode_token(token)\n except Exception:\n return # Already invalid — no need to blacklist\n\n jti = payload.get(\"jti\")\n exp_raw = payload.get(\"exp\")\n if not jti or not exp_raw:\n return\n\n if isinstance(exp_raw, datetime):\n expires_at = exp_raw\n else:\n expires_at = datetime.fromtimestamp(exp_raw, tz=timezone.utc)\n\n existing = db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first()\n if existing:\n return\n\n entry = TokenBlacklist(\n jti=jti,\n token_hash=_hash_token(token),\n expires_at=expires_at,\n )\n db.add(entry)\n db.commit()\n\n\n# #endregion blacklist_token\n\n\n# #region is_token_blacklisted [TYPE Function]\n# @BRIEF Check if a JWT token has been revoked.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Returns True if token is blacklisted, False otherwise.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef is_token_blacklisted(token: str, db: Session) -> bool:\n with belief_scope(\"is_token_blacklisted\"):\n try:\n payload = decode_token(token)\n except Exception:\n return True # Invalid tokens are treated as blacklisted\n\n jti = payload.get(\"jti\")\n if not jti:\n return False\n\n return db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first() is not None\n\n\n# #endregion is_token_blacklisted\n\n# #endregion AuthJwtModule\n" + "body": "# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token]\n#\n# @BRIEF JWT token generation and validation logic with server-side revocation.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [auth_config]\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\n#\n# @INVARIANT Tokens must include aud, iss, iat, jti, exp, and sub claims.\n# Revoked tokens are rejected even if not expired.\n# @PRE JWT secret configured in environment\n# @POST Token encode/decode/revoke functions exported\n# @SIDE_EFFECT None\n# @DATA_CONTRACT TokenPayload -> JWT string\n\nfrom datetime import UTC, datetime, timedelta\nimport hashlib\nimport uuid\n\nfrom jose import jwt\nfrom sqlalchemy.orm import Session\n\nfrom ...models.auth import TokenBlacklist\nfrom ..logger import belief_scope\nfrom .config import auth_config\n\n\n# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token with aud, iss, iat, jti, exp claims.\n# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST Returns a signed JWT string with unique jti for revocation.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n now = datetime.now(UTC)\n if expires_delta:\n expire = now + expires_delta\n else:\n expire = now + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\n \"exp\": expire,\n \"iat\": now,\n \"jti\": str(uuid.uuid4()),\n \"aud\": auth_config.JWT_AUDIENCE,\n \"iss\": auth_config.JWT_ISSUER,\n })\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n\n\n# #region decode_token [TYPE Function]\n# @BRIEF Decodes and validates a JWT token — verifies aud, iss, exp, and blacklist.\n# @PRE token is a signed JWT string.\n# @POST Returns the decoded payload if valid. Raises JWTError if invalid.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef decode_token(token: str) -> dict:\n with belief_scope(\"decode_token\"):\n payload = jwt.decode(\n token,\n auth_config.SECRET_KEY,\n algorithms=[auth_config.ALGORITHM],\n audience=auth_config.JWT_AUDIENCE,\n issuer=auth_config.JWT_ISSUER,\n options={\n \"verify_aud\": True,\n \"verify_exp\": True,\n \"verify_iat\": True,\n \"require\": [\"exp\", \"sub\", \"aud\", \"iss\", \"iat\", \"jti\"],\n },\n )\n return payload\n\n\n# #endregion decode_token\n\n\n# #region _hash_token [TYPE Function]\n# @BRIEF SHA-256 hash of a token string for blacklist lookup.\n# @PRE token is a non-empty JWT string.\n# @POST Returns 64-character hex digest.\ndef _hash_token(token: str) -> str:\n return hashlib.sha256(token.encode()).hexdigest()\n\n\n# #endregion _hash_token\n\n\n# #region _prune_blacklist [TYPE Function]\n# @BRIEF Delete expired entries from the token blacklist.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Expired blacklist rows are removed.\ndef _prune_blacklist(db: Session) -> None:\n now = datetime.now(UTC)\n db.query(TokenBlacklist).filter(TokenBlacklist.expires_at < now).delete()\n db.commit()\n\n\n# #endregion _prune_blacklist\n\n\n# #region blacklist_token [TYPE Function]\n# @BRIEF Revoke a JWT token by adding its hash to the blacklist.\n# @PRE token was issued by this service and hasn't expired.\n# @POST Token is blacklisted and subsequent decode_token calls will reject it.\n# @SIDE_EFFECT Writes to token_blacklist table; prunes expired entries.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef blacklist_token(token: str, db: Session) -> None:\n with belief_scope(\"blacklist_token\"):\n _prune_blacklist(db)\n try:\n payload = decode_token(token)\n except Exception:\n return # Already invalid — no need to blacklist\n\n jti = payload.get(\"jti\")\n exp_raw = payload.get(\"exp\")\n if not jti or not exp_raw:\n return\n\n if isinstance(exp_raw, datetime):\n expires_at = exp_raw\n else:\n expires_at = datetime.fromtimestamp(exp_raw, tz=UTC)\n\n existing = db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first()\n if existing:\n return\n\n entry = TokenBlacklist(\n jti=jti,\n token_hash=_hash_token(token),\n expires_at=expires_at,\n )\n db.add(entry)\n db.commit()\n\n\n# #endregion blacklist_token\n\n\n# #region is_token_blacklisted [TYPE Function]\n# @BRIEF Check if a JWT token has been revoked.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Returns True if token is blacklisted, False otherwise.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef is_token_blacklisted(token: str, db: Session) -> bool:\n with belief_scope(\"is_token_blacklisted\"):\n try:\n payload = decode_token(token)\n except Exception:\n return True # Invalid tokens are treated as blacklisted\n\n jti = payload.get(\"jti\")\n if not jti:\n return False\n\n return db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first() is not None\n\n\n# #endregion is_token_blacklisted\n\n# #endregion AuthJwtModule\n" }, { "contract_id": "create_access_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 28, - "end_line": 58, + "start_line": 27, + "end_line": 57, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19069,14 +19967,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token with aud, iss, iat, jti, exp claims.\n# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST Returns a signed JWT string with unique jti for revocation.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n now = datetime.now(timezone.utc)\n if expires_delta:\n expire = now + expires_delta\n else:\n expire = now + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\n \"exp\": expire,\n \"iat\": now,\n \"jti\": str(uuid.uuid4()),\n \"aud\": auth_config.JWT_AUDIENCE,\n \"iss\": auth_config.JWT_ISSUER,\n })\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n" + "body": "# #region create_access_token [TYPE Function]\n# @BRIEF Generates a new JWT access token with aud, iss, iat, jti, exp claims.\n# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).\n# @POST Returns a signed JWT string with unique jti for revocation.\n# @RELATION DEPENDS_ON -> [auth_config]\n#\ndef create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:\n with belief_scope(\"create_access_token\"):\n to_encode = data.copy()\n now = datetime.now(UTC)\n if expires_delta:\n expire = now + expires_delta\n else:\n expire = now + timedelta(\n minutes=auth_config.ACCESS_TOKEN_EXPIRE_MINUTES\n )\n\n to_encode.update({\n \"exp\": expire,\n \"iat\": now,\n \"jti\": str(uuid.uuid4()),\n \"aud\": auth_config.JWT_AUDIENCE,\n \"iss\": auth_config.JWT_ISSUER,\n })\n encoded_jwt = jwt.encode(\n to_encode, auth_config.SECRET_KEY, algorithm=auth_config.ALGORITHM\n )\n return encoded_jwt\n\n\n# #endregion create_access_token\n" }, { "contract_id": "decode_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 61, - "end_line": 85, + "start_line": 60, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19101,8 +19999,8 @@ "contract_id": "_hash_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 88, - "end_line": 96, + "start_line": 87, + "end_line": 95, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19120,8 +20018,8 @@ "contract_id": "_prune_blacklist", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 99, - "end_line": 109, + "start_line": 98, + "end_line": 108, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19133,14 +20031,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _prune_blacklist [TYPE Function]\n# @BRIEF Delete expired entries from the token blacklist.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Expired blacklist rows are removed.\ndef _prune_blacklist(db: Session) -> None:\n now = datetime.now(timezone.utc)\n db.query(TokenBlacklist).filter(TokenBlacklist.expires_at < now).delete()\n db.commit()\n\n\n# #endregion _prune_blacklist\n" + "body": "# #region _prune_blacklist [TYPE Function]\n# @BRIEF Delete expired entries from the token blacklist.\n# @PRE db is a valid SQLAlchemy session.\n# @POST Expired blacklist rows are removed.\ndef _prune_blacklist(db: Session) -> None:\n now = datetime.now(UTC)\n db.query(TokenBlacklist).filter(TokenBlacklist.expires_at < now).delete()\n db.commit()\n\n\n# #endregion _prune_blacklist\n" }, { "contract_id": "blacklist_token", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 112, - "end_line": 149, + "start_line": 111, + "end_line": 148, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19160,14 +20058,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region blacklist_token [TYPE Function]\n# @BRIEF Revoke a JWT token by adding its hash to the blacklist.\n# @PRE token was issued by this service and hasn't expired.\n# @POST Token is blacklisted and subsequent decode_token calls will reject it.\n# @SIDE_EFFECT Writes to token_blacklist table; prunes expired entries.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef blacklist_token(token: str, db: Session) -> None:\n with belief_scope(\"blacklist_token\"):\n _prune_blacklist(db)\n try:\n payload = decode_token(token)\n except Exception:\n return # Already invalid — no need to blacklist\n\n jti = payload.get(\"jti\")\n exp_raw = payload.get(\"exp\")\n if not jti or not exp_raw:\n return\n\n if isinstance(exp_raw, datetime):\n expires_at = exp_raw\n else:\n expires_at = datetime.fromtimestamp(exp_raw, tz=timezone.utc)\n\n existing = db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first()\n if existing:\n return\n\n entry = TokenBlacklist(\n jti=jti,\n token_hash=_hash_token(token),\n expires_at=expires_at,\n )\n db.add(entry)\n db.commit()\n\n\n# #endregion blacklist_token\n" + "body": "# #region blacklist_token [TYPE Function]\n# @BRIEF Revoke a JWT token by adding its hash to the blacklist.\n# @PRE token was issued by this service and hasn't expired.\n# @POST Token is blacklisted and subsequent decode_token calls will reject it.\n# @SIDE_EFFECT Writes to token_blacklist table; prunes expired entries.\n# @RELATION DEPENDS_ON -> [TokenBlacklist]\ndef blacklist_token(token: str, db: Session) -> None:\n with belief_scope(\"blacklist_token\"):\n _prune_blacklist(db)\n try:\n payload = decode_token(token)\n except Exception:\n return # Already invalid — no need to blacklist\n\n jti = payload.get(\"jti\")\n exp_raw = payload.get(\"exp\")\n if not jti or not exp_raw:\n return\n\n if isinstance(exp_raw, datetime):\n expires_at = exp_raw\n else:\n expires_at = datetime.fromtimestamp(exp_raw, tz=UTC)\n\n existing = db.query(TokenBlacklist).filter(TokenBlacklist.jti == jti).first()\n if existing:\n return\n\n entry = TokenBlacklist(\n jti=jti,\n token_hash=_hash_token(token),\n expires_at=expires_at,\n )\n db.add(entry)\n db.commit()\n\n\n# #endregion blacklist_token\n" }, { "contract_id": "is_token_blacklisted", "contract_type": "Function", "file_path": "backend/src/core/auth/jwt.py", - "start_line": 152, - "end_line": 171, + "start_line": 151, + "end_line": 170, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19193,7 +20091,7 @@ "contract_type": "Module", "file_path": "backend/src/core/auth/logger.py", "start_line": 1, - "end_line": 72, + "end_line": 71, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -19213,14 +20111,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging]\n#\n# @BRIEF Structured auth logging module for audit trail generation.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [EXT:frontend:core_logger]\n#\n# @INVARIANT Must not log sensitive data like passwords or full tokens.\n# Sensitive field names (password, secret, token, key, authorization)\n# in the details dict are automatically masked.\n# @PRE Auth module initialized\n# @POST Audit logging functions exported\n# @SIDE_EFFECT Writes auth audit log entries\n# @DATA_CONTRACT AuthEvent -> LogEntry\n\nimport copy\nimport re\nfrom datetime import datetime\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\n\n# Sensitive field name patterns — values for these keys are masked in logs\n_SENSITIVE_KEYS = re.compile(\n r\"^(password|secret|token|api_key|api\\.key|authorization|auth|credential|credit_card|ssn)$\",\n re.IGNORECASE,\n)\n\n\ndef _mask_details(details: dict[str, Any] | None) -> dict[str, Any] | None:\n \"\"\"Return a copy of details with sensitive field values replaced by '***'.\"\"\"\n if not details:\n return details\n masked = {}\n for key, value in details.items():\n if isinstance(key, str) and _SENSITIVE_KEYS.match(key):\n masked[key] = \"***\"\n elif isinstance(value, dict):\n masked[key] = _mask_details(value)\n else:\n masked[key] = value\n return masked\n\n\n# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE event_type and username are strings.\n# @POST Security event is written to the application log via %s-formatting to prevent log injection.\n# Sensitive fields in details dict are masked.\n# @RELATION DEPENDS_ON -> [logger]\n# @RATIONALE Uses %s-formatting (not f-strings) to prevent log forging via CRLF injection in\n# user-controlled fields — see [SEC:H-1]. Sensitive fields in details\n# dict are masked to prevent credential leakage — see [SEC:L-1].\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.utcnow().isoformat()\n logger.info(\n \"[AUDIT][%s][%s] User: %s\",\n timestamp,\n event_type,\n username,\n )\n safe_details = _mask_details(details)\n if safe_details:\n logger.info(\n \"[AUDIT][%s][%s] Details: %s\",\n timestamp,\n event_type,\n safe_details,\n )\n# #endregion log_security_event\n\n# #endregion AuthLoggerModule\n" + "body": "# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging]\n#\n# @BRIEF Structured auth logging module for audit trail generation.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [EXT:frontend:core_logger]\n#\n# @INVARIANT Must not log sensitive data like passwords or full tokens.\n# Sensitive field names (password, secret, token, key, authorization)\n# in the details dict are automatically masked.\n# @PRE Auth module initialized\n# @POST Audit logging functions exported\n# @SIDE_EFFECT Writes auth audit log entries\n# @DATA_CONTRACT AuthEvent -> LogEntry\n\nfrom datetime import UTC, datetime\nimport re\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\n\n# Sensitive field name patterns — values for these keys are masked in logs\n_SENSITIVE_KEYS = re.compile(\n r\"^(password|secret|token|api_key|api\\.key|authorization|auth|credential|credit_card|ssn)$\",\n re.IGNORECASE,\n)\n\n\ndef _mask_details(details: dict[str, Any] | None) -> dict[str, Any] | None:\n \"\"\"Return a copy of details with sensitive field values replaced by '***'.\"\"\"\n if not details:\n return details\n masked = {}\n for key, value in details.items():\n if isinstance(key, str) and _SENSITIVE_KEYS.match(key):\n masked[key] = \"***\"\n elif isinstance(value, dict):\n masked[key] = _mask_details(value)\n else:\n masked[key] = value\n return masked\n\n\n# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE event_type and username are strings.\n# @POST Security event is written to the application log via %s-formatting to prevent log injection.\n# Sensitive fields in details dict are masked.\n# @RELATION DEPENDS_ON -> [logger]\n# @RATIONALE Uses %s-formatting (not f-strings) to prevent log forging via CRLF injection in\n# user-controlled fields — see [SEC:H-1]. Sensitive fields in details\n# dict are masked to prevent credential leakage — see [SEC:L-1].\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.now(UTC).isoformat()\n logger.info(\n \"[AUDIT][%s][%s] User: %s\",\n timestamp,\n event_type,\n username,\n )\n safe_details = _mask_details(details)\n if safe_details:\n logger.info(\n \"[AUDIT][%s][%s] Details: %s\",\n timestamp,\n event_type,\n safe_details,\n )\n# #endregion log_security_event\n\n# #endregion AuthLoggerModule\n" }, { "contract_id": "log_security_event", "contract_type": "Function", "file_path": "backend/src/core/auth/logger.py", - "start_line": 44, - "end_line": 70, + "start_line": 43, + "end_line": 69, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19232,7 +20130,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE event_type and username are strings.\n# @POST Security event is written to the application log via %s-formatting to prevent log injection.\n# Sensitive fields in details dict are masked.\n# @RELATION DEPENDS_ON -> [logger]\n# @RATIONALE Uses %s-formatting (not f-strings) to prevent log forging via CRLF injection in\n# user-controlled fields — see [SEC:H-1]. Sensitive fields in details\n# dict are masked to prevent credential leakage — see [SEC:L-1].\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.utcnow().isoformat()\n logger.info(\n \"[AUDIT][%s][%s] User: %s\",\n timestamp,\n event_type,\n username,\n )\n safe_details = _mask_details(details)\n if safe_details:\n logger.info(\n \"[AUDIT][%s][%s] Details: %s\",\n timestamp,\n event_type,\n safe_details,\n )\n# #endregion log_security_event\n" + "body": "# #region log_security_event [TYPE Function]\n# @BRIEF Logs a security-related event for audit trails.\n# @PRE event_type and username are strings.\n# @POST Security event is written to the application log via %s-formatting to prevent log injection.\n# Sensitive fields in details dict are masked.\n# @RELATION DEPENDS_ON -> [logger]\n# @RATIONALE Uses %s-formatting (not f-strings) to prevent log forging via CRLF injection in\n# user-controlled fields — see [SEC:H-1]. Sensitive fields in details\n# dict are masked to prevent credential leakage — see [SEC:L-1].\ndef log_security_event(event_type: str, username: str, details: dict = None):\n with belief_scope(\"log_security_event\", f\"{event_type}:{username}\"):\n timestamp = datetime.now(UTC).isoformat()\n logger.info(\n \"[AUDIT][%s][%s] User: %s\",\n timestamp,\n event_type,\n username,\n )\n safe_details = _mask_details(details)\n if safe_details:\n logger.info(\n \"[AUDIT][%s][%s] Details: %s\",\n timestamp,\n event_type,\n safe_details,\n )\n# #endregion log_security_event\n" }, { "contract_id": "AuthOauthModule", @@ -19737,7 +20635,7 @@ "contract_type": "Module", "file_path": "backend/src/core/config_manager.py", "start_line": 1, - "end_line": 632, + "end_line": 640, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -19791,14 +20689,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ConfigManager [C:5] [TYPE Module] [SEMANTICS sqlalchemy, validate, migration, config-manager]\n#\n# @BRIEF Manages application configuration persistence in DB with one-time migration from legacy JSON.\n# @LAYER Domain\n# @PRE Database schema for AppConfigRecord must be initialized.\n# @POST Configuration is loaded into memory and logger is configured.\n# @SIDE_EFFECT Performs DB I/O and may update global logging level.\n# @DATA_CONTRACT Input[json, record] -> Model[AppConfig]\n# @INVARIANT Configuration must always be representable by AppConfig and persisted under global record id.\n# @INVARIANT Environment passwords are encrypted at rest using Fernet (see [SEC:C-1]).\n# @RELATION DEPENDS_ON -> [AppConfig]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION CALLS -> [logger]\n# @RELATION CALLS -> [configure_logger]\n#\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..models.config import AppConfigRecord\nfrom ..models.mapping import Environment as EnvironmentRecord\nfrom .config_models import AppConfig, Environment, GlobalSettings\nfrom .database import SessionLocal\nfrom .encryption import EncryptionManager\nfrom .logger import belief_scope, configure_logger, logger\n\n\n# #region ConfigManager [C:5] [TYPE Class]\n# @BRIEF Handles application configuration load, validation, mutation, and persistence lifecycle.\n# @PRE Database is accessible and AppConfigRecord schema is loaded.\n# @POST Configuration state is synchronized between memory and database.\n# @SIDE_EFFECT Performs DB I/O, OS path validation, and logger reconfiguration.\nclass ConfigManager:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize manager state from persisted or migrated configuration.\n # @PRE config_path is a non-empty string path.\n # @POST self.config is initialized as AppConfig and logger is configured.\n # @SIDE_EFFECT Reads config sources and updates logging configuration.\n # @DATA_CONTRACT Input(str config_path) -> Output(None; self.config: AppConfig)\n def __init__(self, config_path: str = \"config.json\"):\n with belief_scope(\"ConfigManager.__init__\"):\n if not isinstance(config_path, str) or not config_path:\n logger.explore(\n \"Invalid config_path provided\", extra={\"path\": config_path}\n )\n raise ValueError(\"config_path must be a non-empty string\")\n logger.reason(f\"Initializing ConfigManager with legacy path: {config_path}\")\n self.config_path = Path(config_path)\n self.raw_payload: dict[str, Any] = {}\n self.config: AppConfig = self._load_config()\n configure_logger(self.config.settings.logging)\n if not isinstance(self.config, AppConfig):\n logger.explore(\n \"Config loading resulted in invalid type\",\n extra={\"type\": type(self.config)},\n )\n raise TypeError(\"self.config must be an instance of AppConfig\")\n logger.reflect(\"ConfigManager initialization complete\")\n # #endregion __init__\n\n # #region _get_encryption_manager [TYPE Function]\n # @PURPOSE: Lazy-init EncryptionManager for password encryption/decryption.\n # @POST Returns EncryptionManager or None if ENCRYPTION_KEY is unavailable.\n # @SIDE_EFFECT May log a warning if key is missing.\n # @RATIONALE Encryption manager cannot be initialized before ensure_encryption_key()\n # is called at startup. This lazy init allows early bootstrap paths\n # (legacy migration, test setup) to work without encryption.\n def _get_encryption_manager(self) -> EncryptionManager | None:\n if not hasattr(self, \"_encryption\"):\n try:\n self._encryption = EncryptionManager()\n except (RuntimeError, Exception) as exc:\n logger.reason(\n \"EncryptionManager unavailable; passwords stored in plaintext. \"\n \"Ensure ENCRYPTION_KEY is set for production.\",\n extra={\"error\": str(exc)},\n )\n self._encryption = None\n return self._encryption\n # #endregion _get_encryption_manager\n\n # #region _encrypt_env_passwords [TYPE Function]\n # @PURPOSE: Encrypt all environment passwords in the raw payload dict before DB persistence.\n # @PRE payload[\"environments\"] is a list of dicts with optional \"password\" keys.\n # @POST Password fields are encrypted in-place (skips masked/empty/already-encrypted).\n # @SIDE_EFFECT Reads ENCRYPTION_KEY; logs warnings on failure.\n def _encrypt_env_passwords(self, payload: dict) -> None:\n encryption = self._get_encryption_manager()\n if encryption is None:\n return\n environments = payload.get(\"environments\", [])\n if not isinstance(environments, list):\n return\n for env in environments:\n pwd = env.get(\"password\", \"\")\n if not pwd or pwd == \"********\":\n continue\n # Skip if already encrypted (safe-decrypt check)\n try:\n encryption.decrypt(pwd)\n continue\n except Exception:\n pass\n try:\n env[\"password\"] = encryption.encrypt(pwd)\n except Exception as exc:\n logger.explore(\n \"Failed to encrypt environment password\",\n extra={\"error\": str(exc)},\n )\n # #endregion _encrypt_env_passwords\n\n # #region _decrypt_env_passwords [TYPE Function]\n # @PURPOSE: Decrypt all environment passwords in the AppConfig after loading from DB.\n # @PRE config.environments is populated from DB payload.\n # @POST Password fields are decrypted in-place (skips plaintext legacy values).\n def _decrypt_env_passwords(self, config: AppConfig) -> None:\n encryption = self._get_encryption_manager()\n if encryption is None:\n return\n for env in config.environments:\n if not env.password:\n continue\n try:\n env.password = encryption.decrypt(env.password)\n except Exception:\n pass # Assume plaintext (legacy data or test fixture)\n # #endregion _decrypt_env_passwords\n\n # #region _apply_features_from_env [TYPE Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT Reads os.environ; mutates settings.features in-place.\n # @RATIONALE Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n logger.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n extra={\"value\": parsed, \"raw\": dataset_review_env},\n )\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n logger.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n extra={\"value\": parsed, \"raw\": health_monitor_env},\n )\n\n app_timezone_env = os.getenv(\"APP_TIMEZONE\")\n if app_timezone_env is not None:\n settings.app_timezone = app_timezone_env.strip()\n logger.reason(\n \"Applied APP_TIMEZONE from env\",\n extra={\"value\": settings.app_timezone},\n )\n # #endregion _apply_features_from_env\n # #region _default_config [TYPE Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n logger.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n # #endregion _default_config\n # #region _sync_raw_payload_from_config [TYPE 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 logger.reason(\n \"Synchronized raw payload from typed config\",\n extra={\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\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n # #endregion _sync_raw_payload_from_config\n # #region _load_from_legacy_file [TYPE 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 logger.reason(\n \"Legacy config file not found; using default payload\",\n extra={\"path\": str(self.config_path)},\n )\n return {}\n logger.reason(\n \"Loading legacy config file\", extra={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n if not isinstance(payload, dict):\n logger.explore(\n \"Legacy config payload is not a JSON object\",\n extra={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n },\n )\n raise ValueError(\"Legacy config payload must be a JSON object\")\n logger.reason(\n \"Legacy config file loaded successfully\",\n extra={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n # #endregion _load_from_legacy_file\n # #region _get_record [TYPE Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> AppConfigRecord | None:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n logger.reason(\n \"Resolved app config record\", extra={\"exists\": record is not None}\n )\n return record\n # #endregion _get_record\n # #region _load_config [TYPE 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 logger.reason(\n \"Loading configuration from database\",\n extra={\"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 # Decrypt environment passwords after loading from DB (see [SEC:C-1])\n self._decrypt_env_passwords(config)\n self._sync_environment_records(session, config)\n session.commit()\n logger.reason(\n \"Database configuration validated successfully\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n logger.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n extra={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\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 # Legacy config is plaintext — decrypt is a no-op but safe\n self._decrypt_env_passwords(config)\n self._apply_features_from_env(config.settings)\n logger.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n extra={\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 logger.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 logger.explore(\n \"Recoverable config load failure; falling back to default configuration\",\n extra={\"error\": str(exc), \"legacy_path\": str(self.config_path)},\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n logger.explore(\n \"Critical config load failure; re-raising persistence or validation error\",\n extra={\"error\": str(exc)},\n )\n raise\n finally:\n session.close()\n # #endregion _load_config\n # #region _sync_environment_records [TYPE 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 for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\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 record = persisted_by_id.get(normalized_id)\n if record is None:\n logger.reason(\n \"Creating relational environment record from typed config\",\n extra={\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 record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n # #endregion _sync_environment_records\n # #region _delete_stale_environment_records [TYPE Function]\n # @PURPOSE: Remove persisted environment records that are no longer in the configured environments.\n # @PRE _sync_environment_records must already have run so the query returns a current view.\n # @POST Stale Environment rows are deleted via the session (caller must commit).\n # @SIDE_EFFECT Only safe to call during explicit save (_save_config_to_db), NOT during _load_config,\n # because FK-dependent rows (task_records, database_mappings, etc.) may reference deleted rows.\n def _delete_stale_environment_records(\n self, session: Session, config: AppConfig\n ) -> None:\n with belief_scope(\"ConfigManager._delete_stale_environment_records\"):\n persisted_records = session.query(EnvironmentRecord).all()\n configured_ids = {\n str(env.id or \"\").strip()\n for env in (config.environments or [])\n if str(env.id or \"\").strip()\n }\n for record in persisted_records:\n record_id = str(record.id or \"\").strip()\n if record_id and record_id not in configured_ids:\n logger.reason(\n \"Deleting stale environment record\",\n extra={\"environment_id\": record_id},\n )\n session.delete(record)\n # #endregion _delete_stale_environment_records\n # #region _save_config_to_db [TYPE Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n # @SIDE_EFFECT Encrypts environment passwords for at-rest security (see [SEC:C-1]);\n # restores plaintext after save so in-memory config stays usable.\n def _save_config_to_db(\n self, config: AppConfig, session: Session | None = 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 # Encrypt passwords for at-rest security before DB write\n self._encrypt_env_passwords(payload)\n record = self._get_record(db)\n if record is None:\n logger.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n logger.reason(\n \"Updating existing global app config record\",\n extra={\"record_id\": record.id},\n )\n record.payload = payload\n self._sync_environment_records(db, config)\n self._delete_stale_environment_records(db, config)\n db.commit()\n logger.reason(\n \"Configuration persisted to database\",\n extra={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n # Restore plaintext passwords in config after save\n self._decrypt_env_passwords(config)\n except Exception:\n db.rollback()\n logger.explore(\"Database save failed; transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n # #endregion _save_config_to_db\n # #region save [TYPE Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n logger.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n # #endregion save\n # #region get_config [TYPE 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 # #endregion get_config\n # #region get_payload [TYPE 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 # #endregion get_payload\n # #region save_config [TYPE 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 logger.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 if isinstance(config, dict):\n logger.reason(\n \"Saving raw config payload\",\n extra={\"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 logger.explore(\n \"Unsupported config type supplied to save_config\",\n extra={\"type\": type(config).__name__},\n )\n raise TypeError(\"config must be AppConfig or dict\")\n # #endregion save_config\n # #region update_global_settings [TYPE 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 logger.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n # #endregion update_global_settings\n # #region validate_path [TYPE 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 if not target.exists():\n return False, f\"Path does not exist: {target}\"\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\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 logger.reason(\"Path validation succeeded\", extra={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n logger.explore(\n \"Path validation failed\", extra={\"path\": path, \"error\": str(exc)}\n )\n return False, str(exc)\n # #endregion validate_path\n # #region get_environments [TYPE 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 # #endregion get_environments\n # #region has_environments [TYPE 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 # #endregion has_environments\n # #region get_environment [TYPE Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Environment | None:\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 for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n # #endregion get_environment\n # #region add_environment [TYPE 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 if existing_index is None:\n logger.reason(\"Appending new environment\", extra={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n logger.reason(\n \"Replacing existing environment during add\",\n extra={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\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 self.save()\n return self.config\n # #endregion add_environment\n # #region update_environment [TYPE 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 update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n updated = Environment.model_validate(update_data)\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 self.config.environments[index] = updated\n logger.reason(\"Environment updated\", extra={\"env_id\": env_id})\n self.save()\n return True\n logger.explore(\n \"Environment update skipped; env not found\", extra={\"env_id\": env_id}\n )\n return False\n # #endregion update_environment\n # #region delete_environment [TYPE 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 if len(self.config.environments) == before:\n logger.explore(\n \"Environment delete skipped; env not found\",\n extra={\"env_id\": env_id},\n )\n return False\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\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 logger.reason(\n \"Environment deleted\",\n extra={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n # #endregion delete_environment\n# #endregion ConfigManager\n# #endregion ConfigManager\n" + "body": "# #region ConfigManager [C:5] [TYPE Module] [SEMANTICS sqlalchemy, validate, migration, config-manager]\n#\n# @BRIEF Manages application configuration persistence in DB with one-time migration from legacy JSON.\n# @LAYER Domain\n# @PRE Database schema for AppConfigRecord must be initialized.\n# @POST Configuration is loaded into memory and logger is configured.\n# @SIDE_EFFECT Performs DB I/O and may update global logging level.\n# @DATA_CONTRACT Input[json, record] -> Model[AppConfig]\n# @INVARIANT Configuration must always be representable by AppConfig and persisted under global record id.\n# @INVARIANT Environment passwords are encrypted at rest using Fernet (see [SEC:C-1]).\n# @RELATION DEPENDS_ON -> [AppConfig]\n# @RELATION DEPENDS_ON -> [SessionLocal]\n# @RELATION DEPENDS_ON -> [AppConfigRecord]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION CALLS -> [logger]\n# @RELATION CALLS -> [configure_logger]\n#\nimport json\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..models.config import AppConfigRecord\nfrom ..models.mapping import Environment as EnvironmentRecord\nfrom .config_models import AppConfig, Environment, GlobalSettings\nfrom .database import SessionLocal\nfrom .encryption import EncryptionManager\nfrom .logger import belief_scope, configure_logger, logger\n\n\n# #region ConfigManager [C:5] [TYPE Class]\n# @BRIEF Handles application configuration load, validation, mutation, and persistence lifecycle.\n# @PRE Database is accessible and AppConfigRecord schema is loaded.\n# @POST Configuration state is synchronized between memory and database.\n# @SIDE_EFFECT Performs DB I/O, OS path validation, and logger reconfiguration.\nclass ConfigManager:\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initialize manager state from persisted or migrated configuration.\n # @PRE config_path is a non-empty string path.\n # @POST self.config is initialized as AppConfig and logger is configured.\n # @SIDE_EFFECT Reads config sources and updates logging configuration.\n # @DATA_CONTRACT Input(str config_path) -> Output(None; self.config: AppConfig)\n def __init__(self, config_path: str = \"config.json\"):\n with belief_scope(\"ConfigManager.__init__\"):\n if not isinstance(config_path, str) or not config_path:\n logger.explore(\n \"Invalid config_path provided\", extra={\"path\": config_path}\n )\n raise ValueError(\"config_path must be a non-empty string\")\n logger.reason(f\"Initializing ConfigManager with legacy path: {config_path}\")\n self.config_path = Path(config_path)\n self.raw_payload: dict[str, Any] = {}\n self.config: AppConfig = self._load_config()\n configure_logger(self.config.settings.logging)\n # Disable propagation on app loggers to prevent duplicate output\n # via uvicorn's root logger handler (see ADR LOG-002 in logger.py).\n # We do this caller-side so tests calling configure_logger directly\n # retain propagation for caplog capture.\n from .cot_logger import cot_logger # noqa: F811\n from .logger import logger as _app_logger\n _app_logger.propagate = False # type: ignore[assignment]\n cot_logger.propagate = False\n if not isinstance(self.config, AppConfig):\n logger.explore(\n \"Config loading resulted in invalid type\",\n extra={\"type\": type(self.config)},\n )\n raise TypeError(\"self.config must be an instance of AppConfig\")\n logger.reflect(\"ConfigManager initialization complete\")\n # #endregion __init__\n\n # #region _get_encryption_manager [TYPE Function]\n # @PURPOSE: Lazy-init EncryptionManager for password encryption/decryption.\n # @POST Returns EncryptionManager or None if ENCRYPTION_KEY is unavailable.\n # @SIDE_EFFECT May log a warning if key is missing.\n # @RATIONALE Encryption manager cannot be initialized before ensure_encryption_key()\n # is called at startup. This lazy init allows early bootstrap paths\n # (legacy migration, test setup) to work without encryption.\n def _get_encryption_manager(self) -> EncryptionManager | None:\n if not hasattr(self, \"_encryption\"):\n try:\n self._encryption = EncryptionManager()\n except (RuntimeError, Exception) as exc:\n logger.reason(\n \"EncryptionManager unavailable; passwords stored in plaintext. \"\n \"Ensure ENCRYPTION_KEY is set for production.\",\n extra={\"error\": str(exc)},\n )\n self._encryption = None\n return self._encryption\n # #endregion _get_encryption_manager\n\n # #region _encrypt_env_passwords [TYPE Function]\n # @PURPOSE: Encrypt all environment passwords in the raw payload dict before DB persistence.\n # @PRE payload[\"environments\"] is a list of dicts with optional \"password\" keys.\n # @POST Password fields are encrypted in-place (skips masked/empty/already-encrypted).\n # @SIDE_EFFECT Reads ENCRYPTION_KEY; logs warnings on failure.\n def _encrypt_env_passwords(self, payload: dict) -> None:\n encryption = self._get_encryption_manager()\n if encryption is None:\n return\n environments = payload.get(\"environments\", [])\n if not isinstance(environments, list):\n return\n for env in environments:\n pwd = env.get(\"password\", \"\")\n if not pwd or pwd == \"********\":\n continue\n # Skip if already encrypted (safe-decrypt check)\n try:\n encryption.decrypt(pwd)\n continue\n except Exception:\n pass\n try:\n env[\"password\"] = encryption.encrypt(pwd)\n except Exception as exc:\n logger.explore(\n \"Failed to encrypt environment password\",\n extra={\"error\": str(exc)},\n )\n # #endregion _encrypt_env_passwords\n\n # #region _decrypt_env_passwords [TYPE Function]\n # @PURPOSE: Decrypt all environment passwords in the AppConfig after loading from DB.\n # @PRE config.environments is populated from DB payload.\n # @POST Password fields are decrypted in-place (skips plaintext legacy values).\n def _decrypt_env_passwords(self, config: AppConfig) -> None:\n encryption = self._get_encryption_manager()\n if encryption is None:\n return\n for env in config.environments:\n if not env.password:\n continue\n try:\n env.password = encryption.decrypt(env.password)\n except Exception:\n pass # Assume plaintext (legacy data or test fixture)\n # #endregion _decrypt_env_passwords\n\n # #region _apply_features_from_env [TYPE Function]\n # @PURPOSE: Read FEATURES__* env vars and apply them to a GlobalSettings features config.\n # @SIDE_EFFECT Reads os.environ; mutates settings.features in-place.\n # @RATIONALE Env vars seed the initial defaults. After first bootstrap, DB is source of truth.\n @staticmethod\n def _apply_features_from_env(settings: GlobalSettings) -> None:\n with belief_scope(\"ConfigManager._apply_features_from_env\"):\n dataset_review_env = os.getenv(\"FEATURES__DATASET_REVIEW\")\n if dataset_review_env is not None:\n parsed = dataset_review_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.dataset_review = parsed\n logger.reason(\n \"Applied FEATURES__DATASET_REVIEW from env\",\n extra={\"value\": parsed, \"raw\": dataset_review_env},\n )\n health_monitor_env = os.getenv(\"FEATURES__HEALTH_MONITOR\")\n if health_monitor_env is not None:\n parsed = health_monitor_env.strip().lower() in (\"true\", \"1\", \"yes\")\n settings.features.health_monitor = parsed\n logger.reason(\n \"Applied FEATURES__HEALTH_MONITOR from env\",\n extra={\"value\": parsed, \"raw\": health_monitor_env},\n )\n\n app_timezone_env = os.getenv(\"APP_TIMEZONE\")\n if app_timezone_env is not None:\n settings.app_timezone = app_timezone_env.strip()\n logger.reason(\n \"Applied APP_TIMEZONE from env\",\n extra={\"value\": settings.app_timezone},\n )\n # #endregion _apply_features_from_env\n # #region _default_config [TYPE Function]\n # @PURPOSE: Build default application configuration fallback.\n def _default_config(self) -> AppConfig:\n with belief_scope(\"ConfigManager._default_config\"):\n logger.reason(\"Building default AppConfig fallback\")\n config = AppConfig(environments=[], settings=GlobalSettings())\n self._apply_features_from_env(config.settings)\n return config\n # #endregion _default_config\n # #region _sync_raw_payload_from_config [TYPE 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 logger.reason(\n \"Synchronized raw payload from typed config\",\n extra={\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\n if key not in {\"environments\", \"settings\"}\n ),\n },\n )\n return merged_payload\n # #endregion _sync_raw_payload_from_config\n # #region _load_from_legacy_file [TYPE 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 logger.reason(\n \"Legacy config file not found; using default payload\",\n extra={\"path\": str(self.config_path)},\n )\n return {}\n logger.reason(\n \"Loading legacy config file\", extra={\"path\": str(self.config_path)}\n )\n with self.config_path.open(\"r\", encoding=\"utf-8\") as fh:\n payload = json.load(fh)\n if not isinstance(payload, dict):\n logger.explore(\n \"Legacy config payload is not a JSON object\",\n extra={\n \"path\": str(self.config_path),\n \"type\": type(payload).__name__,\n },\n )\n raise ValueError(\"Legacy config payload must be a JSON object\")\n logger.reason(\n \"Legacy config file loaded successfully\",\n extra={\"path\": str(self.config_path), \"keys\": sorted(payload.keys())},\n )\n return payload\n # #endregion _load_from_legacy_file\n # #region _get_record [TYPE Function]\n # @PURPOSE: Resolve global configuration record from DB.\n def _get_record(self, session: Session) -> AppConfigRecord | None:\n with belief_scope(\"ConfigManager._get_record\"):\n record = (\n session.query(AppConfigRecord)\n .filter(AppConfigRecord.id == \"global\")\n .first()\n )\n logger.reason(\n \"Resolved app config record\", extra={\"exists\": record is not None}\n )\n return record\n # #endregion _get_record\n # #region _load_config [TYPE 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 logger.reason(\n \"Loading configuration from database\",\n extra={\"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 # Decrypt environment passwords after loading from DB (see [SEC:C-1])\n self._decrypt_env_passwords(config)\n self._sync_environment_records(session, config)\n session.commit()\n logger.reason(\n \"Database configuration validated successfully\",\n extra={\n \"environments_count\": len(config.environments),\n \"payload_keys\": sorted(self.raw_payload.keys()),\n },\n )\n return config\n logger.reason(\n \"Database configuration record missing; attempting legacy file migration\",\n extra={\"legacy_path\": str(self.config_path)},\n )\n legacy_payload = self._load_from_legacy_file()\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 # Legacy config is plaintext — decrypt is a no-op but safe\n self._decrypt_env_passwords(config)\n self._apply_features_from_env(config.settings)\n logger.reason(\n \"Legacy payload validated; persisting migrated configuration to database\",\n extra={\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 logger.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 logger.explore(\n \"Recoverable config load failure; falling back to default configuration\",\n extra={\"error\": str(exc), \"legacy_path\": str(self.config_path)},\n )\n config = self._default_config()\n self.raw_payload = config.model_dump()\n return config\n except Exception as exc:\n logger.explore(\n \"Critical config load failure; re-raising persistence or validation error\",\n extra={\"error\": str(exc)},\n )\n raise\n finally:\n session.close()\n # #endregion _load_config\n # #region _sync_environment_records [TYPE 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 for environment in configured_envs:\n normalized_id = str(environment.id or \"\").strip()\n if not normalized_id:\n continue\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 record = persisted_by_id.get(normalized_id)\n if record is None:\n logger.reason(\n \"Creating relational environment record from typed config\",\n extra={\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 record.name = display_name\n record.url = normalized_url\n record.credentials_id = credentials_id\n # #endregion _sync_environment_records\n # #region _delete_stale_environment_records [TYPE Function]\n # @PURPOSE: Remove persisted environment records that are no longer in the configured environments.\n # @PRE _sync_environment_records must already have run so the query returns a current view.\n # @POST Stale Environment rows are deleted via the session (caller must commit).\n # @SIDE_EFFECT Only safe to call during explicit save (_save_config_to_db), NOT during _load_config,\n # because FK-dependent rows (task_records, database_mappings, etc.) may reference deleted rows.\n def _delete_stale_environment_records(\n self, session: Session, config: AppConfig\n ) -> None:\n with belief_scope(\"ConfigManager._delete_stale_environment_records\"):\n persisted_records = session.query(EnvironmentRecord).all()\n configured_ids = {\n str(env.id or \"\").strip()\n for env in (config.environments or [])\n if str(env.id or \"\").strip()\n }\n for record in persisted_records:\n record_id = str(record.id or \"\").strip()\n if record_id and record_id not in configured_ids:\n logger.reason(\n \"Deleting stale environment record\",\n extra={\"environment_id\": record_id},\n )\n session.delete(record)\n # #endregion _delete_stale_environment_records\n # #region _save_config_to_db [TYPE Function]\n # @PURPOSE: Persist provided AppConfig into the global DB configuration record.\n # @SIDE_EFFECT Encrypts environment passwords for at-rest security (see [SEC:C-1]);\n # restores plaintext after save so in-memory config stays usable.\n def _save_config_to_db(\n self, config: AppConfig, session: Session | None = 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 # Encrypt passwords for at-rest security before DB write\n self._encrypt_env_passwords(payload)\n record = self._get_record(db)\n if record is None:\n logger.reason(\"Creating new global app config record\")\n record = AppConfigRecord(id=\"global\", payload=payload)\n db.add(record)\n else:\n logger.reason(\n \"Updating existing global app config record\",\n extra={\"record_id\": record.id},\n )\n record.payload = payload\n self._sync_environment_records(db, config)\n self._delete_stale_environment_records(db, config)\n db.commit()\n logger.reason(\n \"Configuration persisted to database\",\n extra={\n \"environments_count\": len(\n payload.get(\"environments\", []) or []\n ),\n \"payload_keys\": sorted(payload.keys()),\n },\n )\n # Restore plaintext passwords in config after save\n self._decrypt_env_passwords(config)\n except Exception:\n db.rollback()\n logger.explore(\"Database save failed; transaction rolled back\")\n raise\n finally:\n if owns_session:\n db.close()\n # #endregion _save_config_to_db\n # #region save [TYPE Function]\n # @PURPOSE: Persist current in-memory configuration state.\n def save(self) -> None:\n with belief_scope(\"ConfigManager.save\"):\n logger.reason(\"Persisting current in-memory configuration\")\n self._save_config_to_db(self.config)\n # #endregion save\n # #region get_config [TYPE 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 # #endregion get_config\n # #region get_payload [TYPE 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 # #endregion get_payload\n # #region save_config [TYPE 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 logger.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 if isinstance(config, dict):\n logger.reason(\n \"Saving raw config payload\",\n extra={\"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 logger.explore(\n \"Unsupported config type supplied to save_config\",\n extra={\"type\": type(config).__name__},\n )\n raise TypeError(\"config must be AppConfig or dict\")\n # #endregion save_config\n # #region update_global_settings [TYPE 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 logger.reason(\"Updating global settings\")\n self.config.settings = settings\n self.save()\n return self.config\n # #endregion update_global_settings\n # #region validate_path [TYPE 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 if not target.exists():\n return False, f\"Path does not exist: {target}\"\n if not target.is_dir():\n return False, f\"Path is not a directory: {target}\"\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 logger.reason(\"Path validation succeeded\", extra={\"path\": str(target)})\n return True, \"OK\"\n except Exception as exc:\n logger.explore(\n \"Path validation failed\", extra={\"path\": path, \"error\": str(exc)}\n )\n return False, str(exc)\n # #endregion validate_path\n # #region get_environments [TYPE 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 # #endregion get_environments\n # #region has_environments [TYPE 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 # #endregion has_environments\n # #region get_environment [TYPE Function]\n # @PURPOSE: Resolve a configured environment by identifier.\n def get_environment(self, env_id: str) -> Environment | None:\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 for env in self.config.environments:\n if env.id == normalized or env.name == normalized:\n return env\n return None\n # #endregion get_environment\n # #region add_environment [TYPE 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 if existing_index is None:\n logger.reason(\"Appending new environment\", extra={\"env_id\": env.id})\n self.config.environments.append(env)\n else:\n logger.reason(\n \"Replacing existing environment during add\",\n extra={\"env_id\": env.id},\n )\n self.config.environments[existing_index] = env\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 self.save()\n return self.config\n # #endregion add_environment\n # #region update_environment [TYPE 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 update_data = env.model_dump()\n if update_data.get(\"password\") == \"********\":\n update_data[\"password\"] = existing.password\n updated = Environment.model_validate(update_data)\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 self.config.environments[index] = updated\n logger.reason(\"Environment updated\", extra={\"env_id\": env_id})\n self.save()\n return True\n logger.explore(\n \"Environment update skipped; env not found\", extra={\"env_id\": env_id}\n )\n return False\n # #endregion update_environment\n # #region delete_environment [TYPE 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 if len(self.config.environments) == before:\n logger.explore(\n \"Environment delete skipped; env not found\",\n extra={\"env_id\": env_id},\n )\n return False\n if removed and removed[0].is_default and self.config.environments:\n self.config.environments[0].is_default = True\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 logger.reason(\n \"Environment deleted\",\n extra={\"env_id\": env_id, \"remaining\": len(self.config.environments)},\n )\n self.save()\n return True\n # #endregion delete_environment\n# #endregion ConfigManager\n# #endregion ConfigManager\n" }, { "contract_id": "_get_encryption_manager", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 66, - "end_line": 85, + "start_line": 74, + "end_line": 93, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19817,8 +20715,8 @@ "contract_id": "_encrypt_env_passwords", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 87, - "end_line": 116, + "start_line": 95, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19837,8 +20735,8 @@ "contract_id": "_decrypt_env_passwords", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 118, - "end_line": 133, + "start_line": 126, + "end_line": 141, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19856,8 +20754,8 @@ "contract_id": "_apply_features_from_env", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 135, - "end_line": 166, + "start_line": 143, + "end_line": 174, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19875,8 +20773,8 @@ "contract_id": "_default_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 167, - "end_line": 175, + "start_line": 175, + "end_line": 183, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19892,8 +20790,8 @@ "contract_id": "_sync_raw_payload_from_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 176, - "end_line": 200, + "start_line": 184, + "end_line": 208, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19909,8 +20807,8 @@ "contract_id": "_load_from_legacy_file", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 201, - "end_line": 230, + "start_line": 209, + "end_line": 238, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19926,8 +20824,8 @@ "contract_id": "_get_record", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 231, - "end_line": 244, + "start_line": 239, + "end_line": 252, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19943,8 +20841,8 @@ "contract_id": "_load_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 245, - "end_line": 324, + "start_line": 253, + "end_line": 332, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19960,8 +20858,8 @@ "contract_id": "_sync_environment_records", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 325, - "end_line": 366, + "start_line": 333, + "end_line": 374, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19977,8 +20875,8 @@ "contract_id": "_delete_stale_environment_records", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 367, - "end_line": 391, + "start_line": 375, + "end_line": 399, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -19997,8 +20895,8 @@ "contract_id": "_save_config_to_db", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 392, - "end_line": 439, + "start_line": 400, + "end_line": 447, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20015,8 +20913,8 @@ "contract_id": "save", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 440, - "end_line": 446, + "start_line": 448, + "end_line": 454, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20032,8 +20930,8 @@ "contract_id": "get_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 447, - "end_line": 452, + "start_line": 455, + "end_line": 460, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20049,8 +20947,8 @@ "contract_id": "get_payload", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 453, - "end_line": 458, + "start_line": 461, + "end_line": 466, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20066,8 +20964,8 @@ "contract_id": "save_config", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 459, - "end_line": 489, + "start_line": 467, + "end_line": 497, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20083,8 +20981,8 @@ "contract_id": "update_global_settings", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 490, - "end_line": 498, + "start_line": 498, + "end_line": 506, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20100,8 +20998,8 @@ "contract_id": "validate_path", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 499, - "end_line": 521, + "start_line": 507, + "end_line": 529, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20117,8 +21015,8 @@ "contract_id": "get_environments", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 522, - "end_line": 527, + "start_line": 530, + "end_line": 535, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20134,8 +21032,8 @@ "contract_id": "has_environments", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 528, - "end_line": 533, + "start_line": 536, + "end_line": 541, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20151,8 +21049,8 @@ "contract_id": "get_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 534, - "end_line": 545, + "start_line": 542, + "end_line": 553, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20168,8 +21066,8 @@ "contract_id": "add_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 546, - "end_line": 576, + "start_line": 554, + "end_line": 584, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20185,8 +21083,8 @@ "contract_id": "update_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 577, - "end_line": 601, + "start_line": 585, + "end_line": 609, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20202,8 +21100,8 @@ "contract_id": "delete_environment", "contract_type": "Function", "file_path": "backend/src/core/config_manager.py", - "start_line": 602, - "end_line": 630, + "start_line": 610, + "end_line": 638, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -20657,7 +21555,7 @@ "contract_type": "Module", "file_path": "backend/src/core/database.py", "start_line": 1, - "end_line": 776, + "end_line": 836, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -20683,7 +21581,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, connection, session]\n#\n# @BRIEF Configures database connection and session management (PostgreSQL-first).\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [auth_config]\n#\n# @INVARIANT A single engine instance is used for the entire application.\n\nimport os\nfrom pathlib import Path\n\nfrom sqlalchemy import create_engine, inspect, text\nfrom sqlalchemy.orm import sessionmaker\n\n# Import models to ensure they're registered with Base\nfrom ..models import (\n api_key as _api_key_models, # noqa: F401\n assistant as _assistant_models, # noqa: F401\n auth as _auth_models, # noqa: F401\n clean_release as _clean_release_models, # noqa: F401\n config as _config_models, # noqa: F401\n dataset_review as _dataset_review_models, # noqa: F401\n llm as _llm_models, # noqa: F401\n maintenance as _maintenance_models, # noqa: F401\n profile as _profile_models, # noqa: F401\n task as _task_models, # noqa: F401\n)\nfrom ..models.mapping import Base\nfrom .auth.config import auth_config\nfrom .logger import belief_scope, logger\n\n# #region BASE_DIR [C:1] [TYPE Variable]\n# @BRIEF Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# #endregion BASE_DIR\n\n# #region DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the main application database. Read from env; crashes if unset.\n# @RATIONALE DATABASE_URL reads from env (DATABASE_URL or POSTGRES_URL).\n# Crashes at import if unset. DEV_MODE fallback removed in [SEC:C-4].\n# @REJECTED Hardcoded postgres:postgres@localhost in source code rejected — exposes\n# database credentials in version control (Class 1 security violation).\n# DEV_MODE fallback removed — same violation via env toggle.\nDATABASE_URL = os.getenv(\"DATABASE_URL\") or os.getenv(\"POSTGRES_URL\")\nif not DATABASE_URL:\n raise RuntimeError(\n \"DATABASE_URL (or POSTGRES_URL) environment variable is required. \"\n \"Set it before starting the server. \"\n \"For local development, create a .env file or use docker-compose.yml.\"\n )\n# #endregion DATABASE_URL\n\n# #region TASKS_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the tasks execution database.\n# Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance.\nTASKS_DATABASE_URL = os.getenv(\"TASKS_DATABASE_URL\", DATABASE_URL)\n# #endregion TASKS_DATABASE_URL\n\n# #region AUTH_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# #endregion AUTH_DATABASE_URL\n\n\n# #region engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for mappings database.\n# @SIDE_EFFECT Creates database engine and manages connection pool.\ndef _build_engine(db_url: str):\n with belief_scope(\"_build_engine\"):\n if db_url.startswith(\"sqlite\"):\n return create_engine(db_url, connect_args={\"check_same_thread\": False})\n return create_engine(db_url, pool_pre_ping=True)\n\n\nengine = _build_engine(DATABASE_URL)\n# #endregion engine\n\n# #region tasks_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# #endregion tasks_engine\n\n# #region auth_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# #endregion auth_engine\n\n# #region SessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the main mappings database.\n# @PRE engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# #endregion SessionLocal\n\n# #region TasksSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the tasks execution database.\n# @PRE tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# #endregion TasksSessionLocal\n\n# #region AuthSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the authentication database.\n# @PRE auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# #endregion AuthSessionLocal\n\n\n# #region _ensure_user_dashboard_preferences_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table.\n# @PRE bind_engine points to application database where profile table is stored.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"git_username\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR\"\n )\n if \"git_email\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR\"\n )\n if \"git_personal_access_token_encrypted\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN git_personal_access_token_encrypted VARCHAR\"\n )\n if \"start_page\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'\"\n )\n if \"auto_open_task_drawer\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n if \"dashboards_table_density\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'\"\n )\n if \"show_only_slug_dashboards\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_columns\n\n\n# #region _ensure_user_dashboard_preferences_health_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table (health fields).\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_health_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_health_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"telegram_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR\"\n )\n if \"email_address\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR\"\n )\n if \"notify_on_fail\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile health preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_health_columns\n\n\n# #region _ensure_llm_validation_results_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for llm_validation_results table.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_llm_validation_results_columns(bind_engine):\n with belief_scope(\"_ensure_llm_validation_results_columns\"):\n table_name = \"llm_validation_results\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"task_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR\"\n )\n if \"environment_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"ValidationRecord additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_llm_validation_results_columns\n\n\n# #region _ensure_git_server_configs_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for git_server_configs table.\n# @PRE bind_engine points to application database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_git_server_configs_columns(bind_engine):\n with belief_scope(\"_ensure_git_server_configs_columns\"):\n table_name = \"git_server_configs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"default_branch\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"GitServerConfig preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_git_server_configs_columns\n\n\n# #region _ensure_auth_users_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for auth users table.\n# @PRE bind_engine points to authentication database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [auth_engine]\ndef _ensure_auth_users_columns(bind_engine):\n with belief_scope(\"_ensure_auth_users_columns\"):\n table_name = \"users\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"full_name\" not in existing_columns:\n alter_statements.append(\"ALTER TABLE users ADD COLUMN full_name VARCHAR\")\n if \"is_ad_user\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE users ADD COLUMN is_ad_user BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n\n if not alter_statements:\n logger.reason(\n \"Auth users schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n return\n\n logger.reason(\n \"Applying additive auth users schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reason(\n \"Auth users schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Auth users additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_auth_users_columns\n\n\n# #region _ensure_roles_is_admin_column [C:3] [TYPE Function]\n# @BRIEF Add is_admin column to roles table if missing (additive migration).\n# @PRE Database connection is active.\n# @POST roles.is_admin column exists (BOOLEAN, default FALSE).\n# @SIDE_EFFECT Executes ALTER TABLE on the auth database.\ndef _ensure_roles_is_admin_column(bind_engine):\n with belief_scope(\"_ensure_roles_is_admin_column\"):\n table_name = \"roles\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"is_admin\" in existing_columns:\n logger.reason(\"roles.is_admin column already exists\")\n return\n\n alter = \"ALTER TABLE roles ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE\"\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter))\n connection.execute(text(\"UPDATE roles SET is_admin = true WHERE name = 'Admin'\"))\n logger.reason(\"Added roles.is_admin column, updated Admin roles\", extra={\"statement\": alter})\n except Exception as migration_error:\n logger.explore(\n \"roles.is_admin additive migration failed\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# #endregion _ensure_roles_is_admin_column\n\n\n# #region _ensure_filter_source_enum_values [C:3] [TYPE Function]\n# @BRIEF Adds missing FilterSource enum values to the PostgreSQL native filtersource type.\n# @PRE bind_engine points to application database with imported_filters table.\n# @POST New enum values are available without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_filter_source_enum_values(bind_engine):\n with belief_scope(\"_ensure_filter_source_enum_values\"):\n try:\n with bind_engine.connect() as connection:\n # Check if the native enum type exists\n result = connection.execute(\n text(\n \"SELECT t.typname FROM pg_type t \"\n \"JOIN pg_namespace n ON t.typnamespace = n.oid \"\n \"WHERE t.typname = 'filtersource' AND n.nspname = 'public'\"\n )\n )\n if result.fetchone() is None:\n logger.reason(\n \"filtersource enum type does not exist yet; skipping migration\"\n )\n return\n\n # Get existing enum values\n result = connection.execute(\n text(\n \"SELECT e.enumlabel FROM pg_enum e \"\n \"JOIN pg_type t ON e.enumtypid = t.oid \"\n \"WHERE t.typname = 'filtersource' \"\n \"ORDER BY e.enumsortorder\"\n )\n )\n existing_values = {row[0] for row in result.fetchall()}\n\n required_values = [\"SUPERSET_PERMALINK\", \"SUPERSET_NATIVE_FILTERS_KEY\"]\n missing_values = [\n v for v in required_values if v not in existing_values\n ]\n\n if not missing_values:\n logger.reason(\n \"filtersource enum already up to date\",\n extra={\"existing\": sorted(existing_values)},\n )\n return\n\n logger.reason(\n \"Adding missing values to filtersource enum\",\n extra={\"missing\": missing_values},\n )\n for value in missing_values:\n connection.execute(\n text(\n f\"ALTER TYPE filtersource ADD VALUE IF NOT EXISTS '{value}'\"\n )\n )\n connection.commit()\n logger.reason(\n \"filtersource enum migration completed\",\n extra={\"added\": missing_values},\n )\n except Exception as migration_error:\n logger.explore(\n \"FilterSource enum additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_filter_source_enum_values\n\n\n# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE bind_engine points to the application database where dataset review tables are stored.\n# @POST Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n\n\n# #region _ensure_translation_schedules_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for translation_schedules table.\n# @PRE bind_engine points to application database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_translation_schedules_columns(bind_engine):\n with belief_scope(\"_ensure_translation_schedules_columns\"):\n table_name = \"translation_schedules\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"execution_mode\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE translation_schedules \"\n \"ADD COLUMN execution_mode VARCHAR NOT NULL DEFAULT 'full'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"TranslationSchedule additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_translation_schedules_columns\n\n\n# #region _ensure_dictionary_entries_columns [C:3] [TYPE Function]\n# @BRIEF Additive migration for dictionary_entries origin tracking columns.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_dictionary_entries_columns(bind_engine):\n with belief_scope(\"_ensure_dictionary_entries_columns\"):\n table_name = \"dictionary_entries\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"origin_run_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_run_id VARCHAR\"\n )\n if \"origin_row_key\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_row_key VARCHAR\"\n )\n if \"origin_user_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_user_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"DictionaryEntry additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_dictionary_entries_columns\n\n\n# #region init_db [C:3] [TYPE Function]\n# @BRIEF Initializes the database by creating all tables.\n# @PRE engine, tasks_engine and auth_engine are initialized.\n# @POST Database tables created in all databases.\n# @SIDE_EFFECT Creates physical database files if they don't exist.\n# @RELATION CALLS -> [_ensure_filter_source_enum_values]\n# @RELATION CALLS -> [_ensure_dataset_review_session_columns]\n# @RELATION CALLS -> [_ensure_dictionary_entries_columns]\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n _ensure_user_dashboard_preferences_columns(engine)\n _ensure_llm_validation_results_columns(engine)\n _ensure_user_dashboard_preferences_health_columns(engine)\n _ensure_git_server_configs_columns(engine)\n _ensure_auth_users_columns(auth_engine)\n _ensure_filter_source_enum_values(engine)\n _ensure_dataset_review_session_columns(engine)\n _ensure_translation_jobs_columns(engine)\n _ensure_translation_schedules_columns(engine)\n _ensure_dictionary_entries_columns(engine)\n\n\n# #endregion init_db\n\n\n# #region get_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a database session.\n# @PRE SessionLocal is initialized.\n# @POST Session is closed after use.\n# @RELATION DEPENDS_ON -> [SessionLocal]\ndef get_db():\n with belief_scope(\"get_db\"):\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_db\n\n\n# #region get_tasks_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a tasks database session.\n# @PRE TasksSessionLocal is initialized.\n# @POST Session is closed after use.\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\ndef get_tasks_db():\n with belief_scope(\"get_tasks_db\"):\n db = TasksSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_tasks_db\n\n\n# #region get_auth_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting an authentication database session.\n# @PRE AuthSessionLocal is initialized.\n# @POST Session is closed after use.\n# @DATA_CONTRACT None -> Output[EXT:Library:sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthSessionLocal]\ndef get_auth_db():\n with belief_scope(\"get_auth_db\"):\n db = AuthSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_auth_db\n\n# #endregion DatabaseModule\n" + "body": "# #region DatabaseModule [C:3] [TYPE Module] [SEMANTICS sqlalchemy, connection, session]\n#\n# @BRIEF Configures database connection and session management (PostgreSQL-first).\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @RELATION DEPENDS_ON -> [auth_config]\n#\n# @INVARIANT A single engine instance is used for the entire application.\n\nimport os\nfrom pathlib import Path\n\nfrom sqlalchemy import create_engine, inspect, text\nfrom sqlalchemy.orm import sessionmaker\n\n# Import models to ensure they're registered with Base\nfrom ..models import (\n api_key as _api_key_models, # noqa: F401\n assistant as _assistant_models, # noqa: F401\n auth as _auth_models, # noqa: F401\n clean_release as _clean_release_models, # noqa: F401\n config as _config_models, # noqa: F401\n dataset_review as _dataset_review_models, # noqa: F401\n llm as _llm_models, # noqa: F401\n maintenance as _maintenance_models, # noqa: F401\n profile as _profile_models, # noqa: F401\n task as _task_models, # noqa: F401\n)\nfrom ..models.mapping import Base\nfrom .auth.config import auth_config\nfrom .logger import belief_scope, logger\n\n# #region BASE_DIR [C:1] [TYPE Variable]\n# @BRIEF Base directory for the backend.\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\n# #endregion BASE_DIR\n\n# #region DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the main application database. Read from env; crashes if unset.\n# @RATIONALE DATABASE_URL reads from env (DATABASE_URL or POSTGRES_URL).\n# Crashes at import if unset. DEV_MODE fallback removed in [SEC:C-4].\n# @REJECTED Hardcoded postgres:postgres@localhost in source code rejected — exposes\n# database credentials in version control (Class 1 security violation).\n# DEV_MODE fallback removed — same violation via env toggle.\nDATABASE_URL = os.getenv(\"DATABASE_URL\") or os.getenv(\"POSTGRES_URL\")\nif not DATABASE_URL:\n raise RuntimeError(\n \"DATABASE_URL (or POSTGRES_URL) environment variable is required. \"\n \"Set it before starting the server. \"\n \"For local development, create a .env file or use docker-compose.yml.\"\n )\n# #endregion DATABASE_URL\n\n# #region TASKS_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the tasks execution database.\n# Defaults to DATABASE_URL to keep task logs in the same PostgreSQL instance.\nTASKS_DATABASE_URL = os.getenv(\"TASKS_DATABASE_URL\", DATABASE_URL)\n# #endregion TASKS_DATABASE_URL\n\n# #region AUTH_DATABASE_URL [C:1] [TYPE Constant]\n# @BRIEF URL for the authentication database.\nAUTH_DATABASE_URL = os.getenv(\"AUTH_DATABASE_URL\", auth_config.AUTH_DATABASE_URL)\n# #endregion AUTH_DATABASE_URL\n\n\n# #region engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for mappings database.\n# @SIDE_EFFECT Creates database engine and manages connection pool.\ndef _build_engine(db_url: str):\n with belief_scope(\"_build_engine\"):\n if db_url.startswith(\"sqlite\"):\n return create_engine(db_url, connect_args={\"check_same_thread\": False})\n return create_engine(db_url, pool_pre_ping=True)\n\n\nengine = _build_engine(DATABASE_URL)\n# #endregion engine\n\n# #region tasks_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for tasks database.\ntasks_engine = _build_engine(TASKS_DATABASE_URL)\n# #endregion tasks_engine\n\n# #region auth_engine [C:1] [TYPE Variable]\n# @BRIEF SQLAlchemy engine for authentication database.\nauth_engine = _build_engine(AUTH_DATABASE_URL)\n# #endregion auth_engine\n\n# #region SessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the main mappings database.\n# @PRE engine is initialized.\nSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n# #endregion SessionLocal\n\n# #region TasksSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the tasks execution database.\n# @PRE tasks_engine is initialized.\nTasksSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=tasks_engine)\n# #endregion TasksSessionLocal\n\n# #region AuthSessionLocal [C:1] [TYPE Class]\n# @BRIEF A session factory for the authentication database.\n# @PRE auth_engine is initialized.\nAuthSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=auth_engine)\n# #endregion AuthSessionLocal\n\n\n# #region _ensure_user_dashboard_preferences_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table.\n# @PRE bind_engine points to application database where profile table is stored.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"git_username\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_username VARCHAR\"\n )\n if \"git_email\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN git_email VARCHAR\"\n )\n if \"git_personal_access_token_encrypted\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN git_personal_access_token_encrypted VARCHAR\"\n )\n if \"start_page\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN start_page VARCHAR NOT NULL DEFAULT 'dashboards'\"\n )\n if \"auto_open_task_drawer\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN auto_open_task_drawer BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n if \"dashboards_table_density\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN dashboards_table_density VARCHAR NOT NULL DEFAULT 'comfortable'\"\n )\n if \"show_only_slug_dashboards\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences \"\n \"ADD COLUMN show_only_slug_dashboards BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_columns\n\n\n# #region _ensure_user_dashboard_preferences_health_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for user_dashboard_preferences table (health fields).\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_user_dashboard_preferences_health_columns(bind_engine):\n with belief_scope(\"_ensure_user_dashboard_preferences_health_columns\"):\n table_name = \"user_dashboard_preferences\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"telegram_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN telegram_id VARCHAR\"\n )\n if \"email_address\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN email_address VARCHAR\"\n )\n if \"notify_on_fail\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE user_dashboard_preferences ADD COLUMN notify_on_fail BOOLEAN NOT NULL DEFAULT TRUE\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"Profile health preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_user_dashboard_preferences_health_columns\n\n\n# #region _ensure_llm_validation_results_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for llm_validation_results table.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_llm_validation_results_columns(bind_engine):\n with belief_scope(\"_ensure_llm_validation_results_columns\"):\n table_name = \"llm_validation_results\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"task_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN task_id VARCHAR\"\n )\n if \"environment_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE llm_validation_results ADD COLUMN environment_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"ValidationRecord additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_llm_validation_results_columns\n\n\n# #region _ensure_git_server_configs_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for git_server_configs table.\n# @PRE bind_engine points to application database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_git_server_configs_columns(bind_engine):\n with belief_scope(\"_ensure_git_server_configs_columns\"):\n table_name = \"git_server_configs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"default_branch\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE git_server_configs ADD COLUMN default_branch VARCHAR NOT NULL DEFAULT 'main'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"GitServerConfig preference additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_git_server_configs_columns\n\n\n# #region _ensure_auth_users_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for auth users table.\n# @PRE bind_engine points to authentication database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [auth_engine]\ndef _ensure_auth_users_columns(bind_engine):\n with belief_scope(\"_ensure_auth_users_columns\"):\n table_name = \"users\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"full_name\" not in existing_columns:\n alter_statements.append(\"ALTER TABLE users ADD COLUMN full_name VARCHAR\")\n if \"is_ad_user\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE users ADD COLUMN is_ad_user BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n\n if not alter_statements:\n logger.reason(\n \"Auth users schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n return\n\n logger.reason(\n \"Applying additive auth users schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reason(\n \"Auth users schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Auth users additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_auth_users_columns\n\n\n# #region _ensure_roles_is_admin_column [C:3] [TYPE Function]\n# @BRIEF Add is_admin column to roles table if missing (additive migration).\n# @PRE Database connection is active.\n# @POST roles.is_admin column exists (BOOLEAN, default FALSE).\n# @SIDE_EFFECT Executes ALTER TABLE on the auth database.\ndef _ensure_roles_is_admin_column(bind_engine):\n with belief_scope(\"_ensure_roles_is_admin_column\"):\n table_name = \"roles\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"is_admin\" in existing_columns:\n logger.reason(\"roles.is_admin column already exists\")\n return\n\n alter = \"ALTER TABLE roles ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE\"\n try:\n with bind_engine.begin() as connection:\n connection.execute(text(alter))\n connection.execute(text(\"UPDATE roles SET is_admin = true WHERE name = 'Admin'\"))\n logger.reason(\"Added roles.is_admin column, updated Admin roles\", extra={\"statement\": alter})\n except Exception as migration_error:\n logger.explore(\n \"roles.is_admin additive migration failed\",\n extra={\"error\": str(migration_error)},\n )\n raise\n# #endregion _ensure_roles_is_admin_column\n\n\n# #region _ensure_filter_source_enum_values [C:3] [TYPE Function]\n# @BRIEF Adds missing FilterSource enum values to the PostgreSQL native filtersource type.\n# @PRE bind_engine points to application database with imported_filters table.\n# @POST New enum values are available without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_filter_source_enum_values(bind_engine):\n with belief_scope(\"_ensure_filter_source_enum_values\"):\n try:\n with bind_engine.connect() as connection:\n # Check if the native enum type exists\n result = connection.execute(\n text(\n \"SELECT t.typname FROM pg_type t \"\n \"JOIN pg_namespace n ON t.typnamespace = n.oid \"\n \"WHERE t.typname = 'filtersource' AND n.nspname = 'public'\"\n )\n )\n if result.fetchone() is None:\n logger.reason(\n \"filtersource enum type does not exist yet; skipping migration\"\n )\n return\n\n # Get existing enum values\n result = connection.execute(\n text(\n \"SELECT e.enumlabel FROM pg_enum e \"\n \"JOIN pg_type t ON e.enumtypid = t.oid \"\n \"WHERE t.typname = 'filtersource' \"\n \"ORDER BY e.enumsortorder\"\n )\n )\n existing_values = {row[0] for row in result.fetchall()}\n\n required_values = [\"SUPERSET_PERMALINK\", \"SUPERSET_NATIVE_FILTERS_KEY\"]\n missing_values = [\n v for v in required_values if v not in existing_values\n ]\n\n if not missing_values:\n logger.reason(\n \"filtersource enum already up to date\",\n extra={\"existing\": sorted(existing_values)},\n )\n return\n\n logger.reason(\n \"Adding missing values to filtersource enum\",\n extra={\"missing\": missing_values},\n )\n for value in missing_values:\n connection.execute(\n text(\n f\"ALTER TYPE filtersource ADD VALUE IF NOT EXISTS '{value}'\"\n )\n )\n connection.commit()\n logger.reason(\n \"filtersource enum migration completed\",\n extra={\"added\": missing_values},\n )\n except Exception as migration_error:\n logger.explore(\n \"FilterSource enum additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_filter_source_enum_values\n\n\n# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE bind_engine points to the application database where dataset review tables are stored.\n# @POST Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n # Columns added to model AFTER initial Alembic migration — no Alembic migration exists\n if \"target_language_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_language_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_language_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_language_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"target_source_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_source_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_source_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_source_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"target_source_language_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_source_language_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_source_language_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_source_language_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"disable_reasoning\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN disable_reasoning BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n )\n logger.reflect(\"Added disable_reasoning column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add disable_reasoning to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n\n\n# #region _ensure_translation_schedules_columns [C:3] [TYPE Function]\n# @BRIEF Applies additive schema upgrades for translation_schedules table.\n# @PRE bind_engine points to application database.\n# @POST Missing columns are added without data loss.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_translation_schedules_columns(bind_engine):\n with belief_scope(\"_ensure_translation_schedules_columns\"):\n table_name = \"translation_schedules\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"execution_mode\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE translation_schedules \"\n \"ADD COLUMN execution_mode VARCHAR NOT NULL DEFAULT 'full'\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"TranslationSchedule additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_translation_schedules_columns\n\n\n# #region _ensure_dictionary_entries_columns [C:3] [TYPE Function]\n# @BRIEF Additive migration for dictionary_entries origin tracking columns.\n# @RELATION DEPENDS_ON -> [engine]\ndef _ensure_dictionary_entries_columns(bind_engine):\n with belief_scope(\"_ensure_dictionary_entries_columns\"):\n table_name = \"dictionary_entries\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n alter_statements = []\n if \"origin_run_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_run_id VARCHAR\"\n )\n if \"origin_row_key\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_row_key VARCHAR\"\n )\n if \"origin_user_id\" not in existing_columns:\n alter_statements.append(\n \"ALTER TABLE dictionary_entries \"\n \"ADD COLUMN origin_user_id VARCHAR\"\n )\n\n if not alter_statements:\n return\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n except Exception as migration_error:\n logger.explore(\n \"DictionaryEntry additive migration failed\",\n extra={\"src\": \"database\", \"error\": str(migration_error)},\n )\n\n\n# #endregion _ensure_dictionary_entries_columns\n\n\n# #region init_db [C:3] [TYPE Function]\n# @BRIEF Creates any missing tables via create_all().\n# @PRE engine, tasks_engine and auth_engine are initialized.\n# Alembic migrations (run_alembic_migrations) have already been applied.\n# @POST All tables from SQLAlchemy models exist in all databases.\n# @SIDE_EFFECT Creates physical database tables if they don't exist.\n# @RATIONALE Schema migrations are handled by Alembic (run_alembic_migrations runs\n# before init_db in startup_event). create_all() only creates NEW tables\n# that don't exist yet — it does NOT alter existing ones. All column\n# additions must go through Alembic migrations.\n# @REJECTED _ensure_*() inline additive migrations removed — they were duplicating\n# Alembic logic, were not versioned, and created hidden schema drift.\n# All schema changes must now go through Alembic.\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n\n\n# #endregion init_db\n\n\n# #region get_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a database session.\n# @PRE SessionLocal is initialized.\n# @POST Session is closed after use.\n# @RELATION DEPENDS_ON -> [SessionLocal]\ndef get_db():\n with belief_scope(\"get_db\"):\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_db\n\n\n# #region get_tasks_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting a tasks database session.\n# @PRE TasksSessionLocal is initialized.\n# @POST Session is closed after use.\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\ndef get_tasks_db():\n with belief_scope(\"get_tasks_db\"):\n db = TasksSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_tasks_db\n\n\n# #region get_auth_db [C:3] [TYPE Function]\n# @BRIEF Dependency for getting an authentication database session.\n# @PRE AuthSessionLocal is initialized.\n# @POST Session is closed after use.\n# @DATA_CONTRACT None -> Output[EXT:Library:sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [AuthSessionLocal]\ndef get_auth_db():\n with belief_scope(\"get_auth_db\"):\n db = AuthSessionLocal()\n try:\n yield db\n finally:\n db.close()\n\n\n# #endregion get_auth_db\n\n# #endregion DatabaseModule\n" }, { "contract_id": "BASE_DIR", @@ -21054,7 +21952,7 @@ "contract_type": "Function", "file_path": "backend/src/core/database.py", "start_line": 474, - "end_line": 604, + "end_line": 669, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -21081,14 +21979,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE bind_engine points to the application database where dataset review tables are stored.\n# @POST Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n" + "body": "# #region _ensure_dataset_review_session_columns [C:4] [TYPE Function]\n# @BRIEF Apply additive schema upgrades for dataset review persistence required by optimistic-lock and recovery metadata semantics.\n# @PRE bind_engine points to the application database where dataset review tables are stored.\n# @POST Missing additive columns across legacy dataset review tables are created without removing existing data.\n# @SIDE_EFFECT Executes ALTER TABLE statements against dataset review tables in the application database.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\ndef _ensure_translation_jobs_columns(bind_engine):\n with belief_scope(\"_ensure_translation_jobs_columns\"):\n table_name = \"translation_jobs\"\n inspector = inspect(bind_engine)\n if table_name not in inspector.get_table_names():\n return\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n\n if \"environment_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN environment_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added environment_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add environment_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n raise\n\n if \"target_database_id\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_database_id VARCHAR\"\n )\n )\n logger.reflect(\n \"Added target_database_id column to translation_jobs\",\n )\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_database_id to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n # Columns added to model AFTER initial Alembic migration — no Alembic migration exists\n if \"target_language_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_language_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_language_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_language_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"target_source_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_source_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_source_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_source_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"target_source_language_column\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN target_source_language_column VARCHAR\"\n )\n )\n logger.reflect(\"Added target_source_language_column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add target_source_language_column to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n if \"disable_reasoning\" not in existing_columns:\n try:\n with bind_engine.begin() as connection:\n connection.execute(\n text(\n \"ALTER TABLE translation_jobs \"\n \"ADD COLUMN disable_reasoning BOOLEAN NOT NULL DEFAULT FALSE\"\n )\n )\n logger.reflect(\"Added disable_reasoning column to translation_jobs\")\n except Exception as migration_error:\n logger.explore(\n \"Failed to add disable_reasoning to translation_jobs\",\n extra={\"error\": str(migration_error)},\n )\n\n\ndef _ensure_dataset_review_session_columns(bind_engine):\n with belief_scope(\"_ensure_dataset_review_session_columns\"):\n inspector = inspect(bind_engine)\n existing_tables = set(inspector.get_table_names())\n migration_plan = {\n \"dataset_review_sessions\": [\n (\n \"version\",\n \"ALTER TABLE dataset_review_sessions \"\n \"ADD COLUMN version INTEGER NOT NULL DEFAULT 0\",\n )\n ],\n \"imported_filters\": [\n (\n \"raw_value_masked\",\n \"ALTER TABLE imported_filters \"\n \"ADD COLUMN raw_value_masked BOOLEAN NOT NULL DEFAULT FALSE\",\n )\n ],\n }\n\n for table_name, planned_columns in migration_plan.items():\n if table_name not in existing_tables:\n logger.reason(\n \"Dataset review table does not exist yet; skipping additive schema migration\",\n extra={\"table\": table_name},\n )\n continue\n\n existing_columns = {\n str(column.get(\"name\") or \"\").strip()\n for column in inspector.get_columns(table_name)\n }\n alter_statements = [\n statement\n for column_name, statement in planned_columns\n if column_name not in existing_columns\n ]\n\n if not alter_statements:\n logger.reason(\n \"Dataset review table schema already up to date\",\n extra={\"table\": table_name, \"columns\": sorted(existing_columns)},\n )\n continue\n\n logger.reason(\n \"Applying additive dataset review schema migration\",\n extra={\"table\": table_name, \"statements\": alter_statements},\n )\n\n try:\n with bind_engine.begin() as connection:\n for statement in alter_statements:\n connection.execute(text(statement))\n logger.reflect(\n \"Dataset review additive schema migration completed\",\n extra={\n \"table\": table_name,\n \"added_columns\": [\n stmt.split(\" ADD COLUMN \", 1)[1].split()[0]\n for stmt in alter_statements\n ],\n },\n )\n except Exception as migration_error:\n logger.explore(\n \"Dataset review additive schema migration failed\",\n extra={\"table\": table_name, \"error\": str(migration_error)},\n )\n raise\n\n\n# #endregion _ensure_dataset_review_session_columns\n" }, { "contract_id": "_ensure_translation_schedules_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 607, - "end_line": 645, + "start_line": 672, + "end_line": 710, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21114,8 +22012,8 @@ "contract_id": "_ensure_dictionary_entries_columns", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 648, - "end_line": 694, + "start_line": 713, + "end_line": 759, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21139,48 +22037,27 @@ "contract_id": "init_db", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 697, - "end_line": 722, + "start_line": 762, + "end_line": 782, "tier": "TIER_2", "complexity": 3, "metadata": { - "BRIEF": "Initializes the database by creating all tables.", + "BRIEF": "Creates any missing tables via create_all().", "COMPLEXITY": 3, - "POST": "Database tables created in all databases.", - "PRE": "engine, tasks_engine and auth_engine are initialized.", - "SIDE_EFFECT": "Creates physical database files if they don't exist." + "PRE": "engine, tasks_engine and auth_engine are initialized." }, - "relations": [ - { - "source_id": "init_db", - "relation_type": "CALLS", - "target_id": "_ensure_filter_source_enum_values", - "target_ref": "[_ensure_filter_source_enum_values]" - }, - { - "source_id": "init_db", - "relation_type": "CALLS", - "target_id": "_ensure_dataset_review_session_columns", - "target_ref": "[_ensure_dataset_review_session_columns]" - }, - { - "source_id": "init_db", - "relation_type": "CALLS", - "target_id": "_ensure_dictionary_entries_columns", - "target_ref": "[_ensure_dictionary_entries_columns]" - } - ], + "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region init_db [C:3] [TYPE Function]\n# @BRIEF Initializes the database by creating all tables.\n# @PRE engine, tasks_engine and auth_engine are initialized.\n# @POST Database tables created in all databases.\n# @SIDE_EFFECT Creates physical database files if they don't exist.\n# @RELATION CALLS -> [_ensure_filter_source_enum_values]\n# @RELATION CALLS -> [_ensure_dataset_review_session_columns]\n# @RELATION CALLS -> [_ensure_dictionary_entries_columns]\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n _ensure_user_dashboard_preferences_columns(engine)\n _ensure_llm_validation_results_columns(engine)\n _ensure_user_dashboard_preferences_health_columns(engine)\n _ensure_git_server_configs_columns(engine)\n _ensure_auth_users_columns(auth_engine)\n _ensure_filter_source_enum_values(engine)\n _ensure_dataset_review_session_columns(engine)\n _ensure_translation_jobs_columns(engine)\n _ensure_translation_schedules_columns(engine)\n _ensure_dictionary_entries_columns(engine)\n\n\n# #endregion init_db\n" + "body": "# #region init_db [C:3] [TYPE Function]\n# @BRIEF Creates any missing tables via create_all().\n# @PRE engine, tasks_engine and auth_engine are initialized.\n# Alembic migrations (run_alembic_migrations) have already been applied.\n# @POST All tables from SQLAlchemy models exist in all databases.\n# @SIDE_EFFECT Creates physical database tables if they don't exist.\n# @RATIONALE Schema migrations are handled by Alembic (run_alembic_migrations runs\n# before init_db in startup_event). create_all() only creates NEW tables\n# that don't exist yet — it does NOT alter existing ones. All column\n# additions must go through Alembic migrations.\n# @REJECTED _ensure_*() inline additive migrations removed — they were duplicating\n# Alembic logic, were not versioned, and created hidden schema drift.\n# All schema changes must now go through Alembic.\ndef init_db():\n with belief_scope(\"init_db\"):\n Base.metadata.create_all(bind=engine)\n Base.metadata.create_all(bind=tasks_engine)\n Base.metadata.create_all(bind=auth_engine)\n\n\n# #endregion init_db\n" }, { "contract_id": "get_db", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 725, - "end_line": 739, + "start_line": 785, + "end_line": 799, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21206,8 +22083,8 @@ "contract_id": "get_tasks_db", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 742, - "end_line": 756, + "start_line": 802, + "end_line": 816, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21233,8 +22110,8 @@ "contract_id": "get_auth_db", "contract_type": "Function", "file_path": "backend/src/core/database.py", - "start_line": 759, - "end_line": 774, + "start_line": 819, + "end_line": 834, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21398,20 +22275,15 @@ "contract_type": "Module", "file_path": "backend/src/core/logger.py", "start_line": 1, - "end_line": 321, + "end_line": 348, "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.", - "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." + "PRE": "Python 3.7+ with cot_logger ContextVars available." }, "relations": [ { @@ -21436,14 +22308,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LoggerModule [C:5] [TYPE Module] [SEMANTICS pydantic, logging, json, formatter, structured]\n# @BRIEF Application logging system with CotJsonFormatter producing molecular CoT JSON output.\n# @LAYER Core\n# @RELATION CALLED_BY -> [LoggerModule]\n# @RELATION DEPENDS_ON -> [CotLoggerModule]\n# @RELATION DEPENDS_ON -> [WebSocketLogHandler]\n# @PRE Python 3.7+ with cot_logger ContextVars available.\n# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state and _task_log_level; writes JSON to console/files.\n# @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.\n# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would confuse log consumers).\n# @DATA_CONTRACT All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}\n# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.\nfrom contextlib import contextmanager\nfrom datetime import UTC, datetime\nimport json\nimport logging\nfrom logging.handlers import RotatingFileHandler\nimport threading\nimport types\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom .cot_logger import _span_id, _trace_id, log as cot_log\nfrom .ws_log_handler import WebSocketLogHandler\n\n# Thread-local storage for belief state\n_belief_state = threading.local()\n\n# Global flag for belief state logging\n_enable_belief_state = True\n\n# Global task log level filter\n_task_log_level = \"INFO\"\n\n\n# #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 -> [CotJsonFormatter]\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 # @INVARIANT Output is always valid single-line JSON.\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 # Default marker for plain messages\n if not marker:\n marker = \"REASON\"\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(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# #endregion CotJsonFormatter\n\n\n# #region BeliefFormatter [C:2] [TYPE Class] [SEMANTICS logging,formatter,legacy,text]\n# @BRIEF Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\n# @RATIONALE Kept for backward compatibility; may be used by external consumers of this module.\nclass BeliefFormatter(logging.Formatter):\n \"\"\"Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\"\"\"\n def format(self, record):\n anchor_id = getattr(_belief_state, 'anchor_id', None)\n if anchor_id:\n msg = str(record.msg)\n markers = (\"[EXPLORE]\", \"[REASON]\", \"[REFLECT]\")\n\n if msg.startswith(f\"[{anchor_id}]\"):\n pass\n elif any(msg.startswith(m) for m in markers):\n record.msg = f\"[{anchor_id}]{msg}\"\n else:\n record.msg = f\"[{anchor_id}][REASON] {msg}\"\n\n return super().format(record)\n# #endregion BeliefFormatter\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region belief_scope [C:3] [TYPE Function] [SEMANTICS logging,context,belief_state,cot]\n# @BRIEF Context manager for Molecular CoT structured logging. Uses cot_logger.log() for JSON output.\n# @PRE anchor_id must be provided.\n# @POST Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.\n@contextmanager\ndef belief_scope(anchor_id: str, message: str = \"\"):\n old_anchor = getattr(_belief_state, 'anchor_id', None)\n _belief_state.anchor_id = anchor_id\n\n try:\n # Log entry as REASON (gated by _enable_belief_state)\n if _enable_belief_state:\n intent_msg = message or f\"Entering {anchor_id}\"\n cot_log(anchor_id, \"REASON\", intent_msg, level=\"DEBUG\")\n yield\n # Log success as REFLECT (always logged)\n cot_log(anchor_id, \"REFLECT\", \"Coherence OK\", level=\"DEBUG\")\n except Exception as e:\n # Log exception as EXPLORE (always logged)\n cot_log(anchor_id, \"EXPLORE\", \"Assumption violated\", error=str(e), level=\"WARNING\")\n raise\n finally:\n _belief_state.anchor_id = old_anchor\n# #endregion belief_scope\n\n\n# #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.\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 # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n# #endregion configure_logger\n\n\n# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\n# @BRIEF Returns the current task log level filter.\ndef get_task_log_level() -> str:\n return _task_log_level\n# #endregion get_task_log_level\n\n\n# #region should_log_task_level [C:1] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on task_log_level setting.\ndef should_log_task_level(level: str) -> bool:\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)\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n# #endregion should_log_task_level\n\n\n# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'EXPLORE', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG 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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n# #endregion LoggerModule\n" + "body": "# #region LoggerModule [C:5] [TYPE Module] [SEMANTICS pydantic, logging, json, formatter, structured]\n# @BRIEF Application logging system with CotJsonFormatter producing molecular CoT JSON output.\n# @LAYER Core\n# @RELATION CALLED_BY -> [LoggerModule]\n# @RELATION DEPENDS_ON -> [CotLoggerModule]\n# @RELATION DEPENDS_ON -> [WebSocketLogHandler]\n# @PRE Python 3.7+ with cot_logger ContextVars available.\n# @POST All log output is single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id.\n# Both 'superset_tools_app' and 'cot' loggers have propagate=False — no text duplicates via root logger.\n# @INVARIANT Every logger.info() call MUST produce exactly one JSON line (see [ADR LOG-002] for why).\n# @SIDE_EFFECT Configures global logger handlers and formatters; seeds global _enable_belief_state\n# and _task_log_level; sets propagate=False on both loggers; writes JSON to console/files.\n# @RELATION DISABLED_BY -> [LOG-001:alembic/env.py:fileConfig]\n# If alembic/env.py calls fileConfig() without disable_existing_loggers=False, Python 3.13's\n# logging framework sets logger.disabled = True on 'superset_tools_app', silencing ALL output.\n# @RATIONALE Replaced BeliefFormatter text-based [Entry]/[Exit]/[Action] logging with CotJsonFormatter\n# JSON output. Old format was not machine-parseable and mixed presentation with intent. CotJsonFormatter\n# produces structured JSON consistent with cot_logger.py MarkerLogger protocol.\n# @REJECTED Keeping both formatters side-by-side was rejected (two inconsistent output formats would\n# confuse log consumers).\n# @REJECTED Keeping propagate=True was rejected after uvicorn's dictConfig added a root logger handler\n# that duplicates every log as plain text (see [ADR LOG-002]).\n# @DATA_CONTRACT All log records conform to molecular CoT JSON schema: {ts, level, trace_id, src, marker, intent, span_id?, payload?, error?}\n# @INVARIANT CotJsonFormatter.format() always returns valid single-line JSON string.\nfrom contextlib import contextmanager\nfrom datetime import UTC, datetime\nimport json\nimport logging\nfrom logging.handlers import RotatingFileHandler\nimport threading\nimport types\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\nfrom .cot_logger import _span_id, _trace_id, log as cot_log\nfrom .ws_log_handler import WebSocketLogHandler\n\n# Thread-local storage for belief state\n_belief_state = threading.local()\n\n# Global flag for belief state logging\n_enable_belief_state = True\n\n# Global task log level filter\n_task_log_level = \"INFO\"\n\n\n# #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 -> [CotJsonFormatter]\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 # @INVARIANT Output is always valid single-line JSON.\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 # Default marker for plain messages\n if not marker:\n marker = \"REASON\"\n if not intent:\n intent = record.getMessage()\n\n log_obj = {\n \"ts\": datetime.now(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# #endregion CotJsonFormatter\n\n\n# #region BeliefFormatter [C:2] [TYPE Class] [SEMANTICS logging,formatter,legacy,text]\n# @BRIEF Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\n# @RATIONALE Kept for backward compatibility; may be used by external consumers of this module.\nclass BeliefFormatter(logging.Formatter):\n \"\"\"Legacy formatter that adds belief state prefixes to log messages. Deprecated in favor of CotJsonFormatter.\"\"\"\n def format(self, record):\n anchor_id = getattr(_belief_state, 'anchor_id', None)\n if anchor_id:\n msg = str(record.msg)\n markers = (\"[EXPLORE]\", \"[REASON]\", \"[REFLECT]\")\n\n if msg.startswith(f\"[{anchor_id}]\"):\n pass\n elif any(msg.startswith(m) for m in markers):\n record.msg = f\"[{anchor_id}]{msg}\"\n else:\n record.msg = f\"[{anchor_id}][REASON] {msg}\"\n\n return super().format(record)\n# #endregion BeliefFormatter\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region belief_scope [C:3] [TYPE Function] [SEMANTICS logging,context,belief_state,cot]\n# @BRIEF Context manager for Molecular CoT structured logging. Uses cot_logger.log() for JSON output.\n# @PRE anchor_id must be provided.\n# @POST Thread-local belief state is updated; REASON/REFLECT/EXPLORE markers emitted via structured JSON logger.\n@contextmanager\ndef belief_scope(anchor_id: str, message: str = \"\"):\n old_anchor = getattr(_belief_state, 'anchor_id', None)\n _belief_state.anchor_id = anchor_id\n\n try:\n # Log entry as REASON (gated by _enable_belief_state)\n if _enable_belief_state:\n intent_msg = message or f\"Entering {anchor_id}\"\n cot_log(anchor_id, \"REASON\", intent_msg, level=\"DEBUG\")\n yield\n # Log success as REFLECT (always logged)\n cot_log(anchor_id, \"REFLECT\", \"Coherence OK\", level=\"DEBUG\")\n except Exception as e:\n # Log exception as EXPLORE (always logged)\n cot_log(anchor_id, \"EXPLORE\", \"Assumption violated\", error=str(e), level=\"WARNING\")\n raise\n finally:\n _belief_state.anchor_id = old_anchor\n# #endregion belief_scope\n\n\n# #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, task log level, and\n# propagate flags (False) are updated. Both loggers emit JSON only — no text duplicates.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers\n# on both logger and cot_logger; sets propagate=False on both; creates file directories.\n# @DATA_CONTRACT configure_logger() -> ENABLES both loggers. Every call to logger.info()\n# MUST produce exactly one JSON line (not zero, not two). See [ADR LOG-001] and [ADR LOG-002].\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 # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n\n # ADR [LOG-002] propagation is disabled by the production caller\n # (ConfigManager) after calling configure_logger(). We do NOT set it here\n # because test fixtures (caplog) rely on propagation to capture records.\n # See config_manager.py:56-58 for the production-side disable.\n # @REJECTED Setting propagate=False inside configure_logger was tried but\n # breaks all caplog-dependent tests. Caller-side disable keeps both\n # testability and production correctness.\n# #endregion configure_logger\n\n\n# #region get_task_log_level [C:1] [TYPE Function] [SEMANTICS logging,configuration,getter]\n# @BRIEF Returns the current task log level filter.\ndef get_task_log_level() -> str:\n return _task_log_level\n# #endregion get_task_log_level\n\n\n# #region should_log_task_level [C:1] [TYPE Function] [SEMANTICS logging,filter,level]\n# @BRIEF Checks if a log level should be recorded based on task_log_level setting.\ndef should_log_task_level(level: str) -> bool:\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)\n check_order = level_order.get(check_level, 1)\n\n return check_order >= current_order\n# #endregion should_log_task_level\n\n\n# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'EXPLORE', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #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.\n# @RATIONALE INFO level per molecular CoT protocol: REASON is the primary reasoning bond\n# and must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,info]\n# @BRIEF Logs a REFLECT marker (INFO level) with structured extra data for CotJsonFormatter.\n# @RATIONALE INFO level per molecular CoT protocol: REFLECT verifies outcomes and\n# must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n# #endregion LoggerModule\n" }, { "contract_id": "CotJsonFormatter", "contract_type": "Class", "file_path": "backend/src/core/logger.py", - "start_line": 38, - "end_line": 99, + "start_line": 49, + "end_line": 110, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21467,8 +22339,8 @@ "contract_id": "CotJsonFormatter.format", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 63, - "end_line": 98, + "start_line": 74, + "end_line": 109, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21486,8 +22358,8 @@ "contract_id": "BeliefFormatter", "contract_type": "Class", "file_path": "backend/src/core/logger.py", - "start_line": 102, - "end_line": 121, + "start_line": 113, + "end_line": 132, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21505,8 +22377,8 @@ "contract_id": "LogEntry", "contract_type": "Class", "file_path": "backend/src/core/logger.py", - "start_line": 124, - "end_line": 131, + "start_line": 135, + "end_line": 142, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21517,14 +22389,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n" + "body": "# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\n# @BRIEF A Pydantic model representing a single, structured log entry.\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n" }, { "contract_id": "belief_scope", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 134, - "end_line": 157, + "start_line": 145, + "end_line": 168, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21543,16 +22415,15 @@ "contract_id": "configure_logger", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 160, - "end_line": 208, + "start_line": 171, + "end_line": 231, "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.", - "PRE": "config is a valid LoggingConfig instance.", - "SIDE_EFFECT": "Mutates global _enable_belief_state, _task_log_level; adds/removes handlers on both logger and cot_logger; creates file directories." + "POST": "Logger levels, handlers, formatters, belief state flag, task log level, and", + "PRE": "config is a valid LoggingConfig instance." }, "relations": [ { @@ -21571,14 +22442,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "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.\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 # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n# #endregion configure_logger\n" + "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, task log level, and\n# propagate flags (False) are updated. Both loggers emit JSON only — no text duplicates.\n# @SIDE_EFFECT Mutates global _enable_belief_state, _task_log_level; adds/removes handlers\n# on both logger and cot_logger; sets propagate=False on both; creates file directories.\n# @DATA_CONTRACT configure_logger() -> ENABLES both loggers. Every call to logger.info()\n# MUST produce exactly one JSON line (not zero, not two). See [ADR LOG-001] and [ADR LOG-002].\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 # Configure the 'cot' logger for consistent JSON output\n from .cot_logger import cot_logger\n cot_logger.setLevel(level)\n if not cot_logger.handlers:\n cot_handler = logging.StreamHandler()\n cot_handler.setFormatter(CotJsonFormatter())\n cot_logger.addHandler(cot_handler)\n\n # ADR [LOG-002] propagation is disabled by the production caller\n # (ConfigManager) after calling configure_logger(). We do NOT set it here\n # because test fixtures (caplog) rely on propagation to capture records.\n # See config_manager.py:56-58 for the production-side disable.\n # @REJECTED Setting propagate=False inside configure_logger was tried but\n # breaks all caplog-dependent tests. Caller-side disable keeps both\n # testability and production correctness.\n# #endregion configure_logger\n" }, { "contract_id": "get_task_log_level", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 211, - "end_line": 215, + "start_line": 234, + "end_line": 238, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21595,8 +22466,8 @@ "contract_id": "should_log_task_level", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 218, - "end_line": 229, + "start_line": 241, + "end_line": 252, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -21613,8 +22484,8 @@ "contract_id": "Logger", "contract_type": "Global", "file_path": "backend/src/core/logger.py", - "start_line": 232, - "end_line": 320, + "start_line": 255, + "end_line": 347, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21625,14 +22496,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'EXPLORE', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG 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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n" + "body": "# #region Logger [C:2] [TYPE Global] [SEMANTICS logger,global,instance]\n# @BRIEF The global application logger instance configured with CotJsonFormatter, console handler, and WebSocketLogHandler.\nlogger = logging.getLogger(\"superset_tools_app\")\n\n# #region believed [C:2] [TYPE Function] [SEMANTICS decorator,belief,scope]\n# @BRIEF A decorator that wraps a function in a belief scope.\ndef believed(anchor_id: str):\n def decorator(func):\n def wrapper(*args, **kwargs):\n with belief_scope(anchor_id):\n return func(*args, **kwargs)\n return wrapper\n return decorator\n# #endregion believed\nlogger.setLevel(logging.INFO)\n\n# Create CotJsonFormatter\n_formatter = CotJsonFormatter()\n\n# Add console handler\nconsole_handler = logging.StreamHandler()\nconsole_handler.setFormatter(_formatter)\nlogger.addHandler(console_handler)\n\n# Add WebSocket log handler\nwebsocket_log_handler = WebSocketLogHandler()\nwebsocket_log_handler.setFormatter(_formatter)\nlogger.addHandler(websocket_log_handler)\n\n# Example usage:\n# logger.info(\"Application started\", extra={\"context_key\": \"context_value\"})\n# logger.error(\"An error occurred\", exc_info=True)\n\n\n# #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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'EXPLORE', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.warning(msg, *args, extra=extra, **kwargs)\n# #endregion explore\n\n\n# #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.\n# @RATIONALE INFO level per molecular CoT protocol: REASON is the primary reasoning bond\n# and must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n\n\n# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,info]\n# @BRIEF Logs a REFLECT marker (INFO level) with structured extra data for CotJsonFormatter.\n# @RATIONALE INFO level per molecular CoT protocol: REFLECT verifies outcomes and\n# must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n\nlogger.explore = types.MethodType(explore, logger)\nlogger.reason = types.MethodType(reason, logger)\nlogger.reflect = types.MethodType(reflect, logger)\n\n# #endregion Logger\n" }, { "contract_id": "believed", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 236, - "end_line": 245, + "start_line": 259, + "end_line": 268, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21649,8 +22520,8 @@ "contract_id": "explore", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 266, - "end_line": 280, + "start_line": 289, + "end_line": 303, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21667,44 +22538,46 @@ "contract_id": "reason", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 283, - "end_line": 297, + "start_line": 306, + "end_line": 322, "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Logs a REASON marker (DEBUG level) with structured extra data for CotJsonFormatter.", - "COMPLEXITY": 2 + "BRIEF": "Logs a REASON marker (INFO level) with structured extra data for CotJsonFormatter.", + "COMPLEXITY": 2, + "RATIONALE": "INFO level per molecular CoT protocol: REASON is the primary reasoning bond" }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region reason [C:2] [TYPE Function] [SEMANTICS log,reason,marker,debug]\n# @BRIEF Logs a REASON marker (DEBUG 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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n" + "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.\n# @RATIONALE INFO level per molecular CoT protocol: REASON is the primary reasoning bond\n# and must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REASON', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reason\n" }, { "contract_id": "reflect", "contract_type": "Function", "file_path": "backend/src/core/logger.py", - "start_line": 300, - "end_line": 314, + "start_line": 325, + "end_line": 341, "tier": "TIER_1", "complexity": 2, "metadata": { - "BRIEF": "Logs a REFLECT marker (DEBUG level) with structured extra data for CotJsonFormatter.", - "COMPLEXITY": 2 + "BRIEF": "Logs a REFLECT marker (INFO level) with structured extra data for CotJsonFormatter.", + "COMPLEXITY": 2, + "RATIONALE": "INFO level per molecular CoT protocol: REFLECT verifies outcomes and" }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.debug(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n" + "body": "# #region reflect [C:2] [TYPE Function] [SEMANTICS log,reflect,marker,info]\n# @BRIEF Logs a REFLECT marker (INFO level) with structured extra data for CotJsonFormatter.\n# @RATIONALE INFO level per molecular CoT protocol: REFLECT verifies outcomes and\n# must be visible in production. DEBUG is reserved for high-frequency loops.\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 error_val = kwargs.pop('error', None)\n extra = {'marker': 'REFLECT', 'intent': msg}\n if error_val is not None:\n extra['error'] = error_val\n extra.update(user_extra)\n self.info(msg, *args, extra=extra, **kwargs)\n# #endregion reflect\n" }, { "contract_id": "test_logger", "contract_type": "Module", "file_path": "backend/src/core/logger/__tests__/test_logger.py", "start_line": 1, - "end_line": 250, + "end_line": 265, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -21723,14 +22596,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_logger [TYPE Module] [C:3] [SEMANTICS test, logger, logging, unit]\n# @BRIEF Unit tests for logger module\n# @LAYER Infrastructure\n# @RELATION BINDS_TO -> [LoggerModule]\nfrom pathlib import Path\nimport sys\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent.parent / \"src\"))\nimport logging\nimport pytest\n\nfrom src.core.config_models import LoggingConfig\nfrom src.core.logger import belief_scope, configure_logger, get_task_log_level, logger, should_log_task_level\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REASON and REFLECT markers at DEBUG level.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n # Check that the logs contain the expected patterns\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"Doing something important\" in msg for msg in log_messages), \"Inline info log not found\"\n assert any(\"[REFLECT]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n# #region test_belief_scope_error_handling [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs EXPLORE on exception.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain EXPLORE marker.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"FailingFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"[EXPLORE]\" in msg and \"FailingFunction\" in msg and \"Something went wrong\" in msg\n for msg in log_messages), \"EXPLORE log not found\"\n # REFLECT should not be logged on failure\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n# #region test_belief_scope_success_reflect [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs REFLECT on success.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_reflect(caplog):\n \"\"\"Test that belief_scope logs REFLECT on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"SuccessFunction\"):\n pass\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REFLECT]\" in msg and \"SuccessFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n# #endregion test_belief_scope_success_reflect\n# #region test_belief_scope_not_visible_at_info [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope REFLECT logs are NOT visible at INFO level.\n# @PRE belief_scope is available. caplog fixture is used.\n# @POST REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).\ndef test_belief_scope_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REFLECT logs are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n log_messages = [record.message for record in caplog.records]\n # Inline INFO log should be visible\n assert any(\"Doing something important\" in msg for msg in log_messages), \"INFO log not found\"\n # REASON uses info() → visible at INFO level\n assert any(\"[REASON]\" in msg and \"InfoLevelFunction\" in msg for msg in log_messages), \"REASON log should be visible at INFO\"\n # REFLECT uses debug() → NOT visible at INFO level\n assert not any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT log should not be visible at INFO\"\n# #endregion test_belief_scope_not_visible_at_info\n# #region test_task_log_level_default [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that default task log level is INFO.\n# @PRE None.\n# @POST Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO (after reset fixture).\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# #endregion test_task_log_level_default\n# #region test_should_log_task_level [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that should_log_task_level correctly filters log levels.\n# @PRE None.\n# @POST Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# #endregion test_should_log_task_level\n# #region test_configure_logger_task_log_level [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that configure_logger updates task_log_level.\n# @PRE LoggingConfig is available.\n# @POST task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n# #endregion test_configure_logger_task_log_level\n# #region test_enable_belief_state_flag [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that enable_belief_state flag controls belief_scope logging.\n# @PRE LoggingConfig is available. caplog fixture is used.\n# @POST belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n log_messages = [record.message for record in caplog.records]\n\n # \"Entering DisabledFunction\" (explicit logger.reason()) should NOT be logged when disabled\n assert not any(\"Entering\" in msg for msg in log_messages), \"REASON entry should not be logged when disabled\"\n # REFLECT should still be logged (internal tracking, not gated by _enable_belief_state)\n assert any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT should still be logged\"\n# #endregion test_enable_belief_state_flag\n# #region test_belief_scope_missing_anchor [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n\n from src.core.logger import belief_scope\n with pytest.raises(TypeError):\n # Missing required positional argument 'anchor_id'\n with belief_scope():\n pass\n# #endregion test_belief_scope_missing_anchor\n# #region test_configure_logger_post_conditions [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n\n from src.core.config_models import LoggingConfig\n import src.core.logger as logger_module\n from src.core.logger import BeliefFormatter, configure_logger, get_task_log_level, logger\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to BeliefFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, BeliefFormatter)\n\n # 4. Global states\n assert logger_module._enable_belief_state is False\n assert get_task_log_level() == \"DEBUG\"\n# #endregion test_configure_logger_post_conditions\n# #endregion test_logger\n" + "body": "# #region test_logger [TYPE Module] [C:3] [SEMANTICS test, logger, logging, unit]\n# @BRIEF Unit tests for logger module\n# @LAYER Infrastructure\n# @RELATION BINDS_TO -> [LoggerModule]\nfrom pathlib import Path\nimport sys\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent.parent.parent / \"src\"))\nimport logging\nimport pytest\n\nfrom src.core.config_models import LoggingConfig\nfrom src.core.logger import belief_scope, configure_logger, get_task_log_level, logger, should_log_task_level\n\n\ndef _marker(record):\n \"\"\"Get molecular CoT marker from a log record (set via extra dict).\"\"\"\n return getattr(record, 'marker', None)\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REASON and REFLECT markers at DEBUG level.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n # Check that the logs contain the expected patterns\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n assert any(_marker(r) == \"REASON\" and \"TestFunction\" in r.getMessage() for r in log_records), \"REASON log not found\"\n assert any(\"Doing something important\" in msg for msg in log_messages), \"Inline info log not found\"\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT log not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n# #region test_belief_scope_error_handling [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs EXPLORE on exception.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain EXPLORE marker.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n log_records = list(caplog.records)\n assert any(_marker(r) == \"REASON\" and \"FailingFunction\" in r.getMessage() for r in log_records), \"REASON log not found\"\n assert any(\n _marker(r) == \"EXPLORE\" and \"Something went wrong\" in (getattr(r, 'error', '') or '')\n for r in log_records\n ), \"EXPLORE log not found\"\n # REFLECT should not be logged on failure\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n# #region test_belief_scope_success_reflect [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs REFLECT on success.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_reflect(caplog):\n \"\"\"Test that belief_scope logs REFLECT on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"SuccessFunction\"):\n pass\n log_records = list(caplog.records)\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT log not found\"\n\n# #endregion test_belief_scope_success_reflect\n# #region test_belief_scope_not_visible_at_info [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope REFLECT logs are NOT visible at INFO level.\n# @PRE belief_scope is available. caplog fixture is used.\n# @POST REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).\ndef test_belief_scope_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REFLECT logs are NOT visible at INFO level.\n\n NOTE: Molecular CoT protocol: belief_scope uses cot_log(level='DEBUG')\n for both REASON (entry) and REFLECT (exit). At INFO level, neither\n should appear in caplog. The inline logger.info() IS visible because\n it goes through superset_tools_app at INFO.\n \"\"\"\n caplog.set_level(\"INFO\")\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n # Inline INFO log should be visible\n assert any(\"Doing something important\" in msg for msg in log_messages), \"INFO log not found\"\n # REASON uses DEBUG level in molecular CoT → NOT visible at INFO\n assert not any(_marker(r) == \"REASON\" for r in log_records), \"REASON is DEBUG-level, should NOT be visible at INFO\"\n # REFLECT uses DEBUG level → NOT visible at INFO level\n assert not any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT is DEBUG-level, should NOT be visible at INFO\"\n# #endregion test_belief_scope_not_visible_at_info\n# #region test_task_log_level_default [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that default task log level is INFO.\n# @PRE None.\n# @POST Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO (after reset fixture).\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# #endregion test_task_log_level_default\n# #region test_should_log_task_level [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that should_log_task_level correctly filters log levels.\n# @PRE None.\n# @POST Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# #endregion test_should_log_task_level\n# #region test_configure_logger_task_log_level [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that configure_logger updates task_log_level.\n# @PRE LoggingConfig is available.\n# @POST task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n# #endregion test_configure_logger_task_log_level\n# #region test_enable_belief_state_flag [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that enable_belief_state flag controls belief_scope logging.\n# @PRE LoggingConfig is available. caplog fixture is used.\n# @POST belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n\n # \"Entering DisabledFunction\" (explicit logger.reason()) should NOT be logged when disabled\n assert not any(\"Entering\" in msg for msg in log_messages), \"REASON entry should not be logged when disabled\"\n # REFLECT should still be logged (internal tracking, not gated by _enable_belief_state)\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT should still be logged\"\n# #endregion test_enable_belief_state_flag\n# #region test_belief_scope_missing_anchor [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n\n from src.core.logger import belief_scope\n with pytest.raises(TypeError), belief_scope():\n pass\n# #endregion test_belief_scope_missing_anchor\n# #region test_configure_logger_post_conditions [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n\n from src.core.config_models import LoggingConfig\n import src.core.logger as logger_module\n from src.core.logger import configure_logger, get_task_log_level, logger\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to CotJsonFormatter (replaced BeliefFormatter)\n from src.core.logger import CotJsonFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, CotJsonFormatter)\n\n # 4. Global states\n assert logger_module._enable_belief_state is False\n assert get_task_log_level() == \"DEBUG\"\n# #endregion test_configure_logger_post_conditions\n# #endregion test_logger\n" }, { "contract_id": "test_belief_scope_logs_reason_reflect_at_debug", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 36, - "end_line": 63, + "start_line": 41, + "end_line": 69, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21749,14 +22622,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REASON and REFLECT markers at DEBUG level.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n # Check that the logs contain the expected patterns\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"Doing something important\" in msg for msg in log_messages), \"Inline info log not found\"\n assert any(\"[REFLECT]\" in msg and \"TestFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n" + "body": "# #region test_belief_scope_logs_reason_reflect_at_debug [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REASON and REFLECT markers at DEBUG level.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates [REASON] and [REFLECT] logs at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n # Check that the logs contain the expected patterns\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n assert any(_marker(r) == \"REASON\" and \"TestFunction\" in r.getMessage() for r in log_records), \"REASON log not found\"\n assert any(\"Doing something important\" in msg for msg in log_messages), \"Inline info log not found\"\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT log not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n" }, { "contract_id": "test_belief_scope_error_handling", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 64, - "end_line": 91, + "start_line": 70, + "end_line": 99, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21775,14 +22648,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_belief_scope_error_handling [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs EXPLORE on exception.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain EXPLORE marker.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REASON]\" in msg and \"FailingFunction\" in msg for msg in log_messages), \"REASON log not found\"\n assert any(\"[EXPLORE]\" in msg and \"FailingFunction\" in msg and \"Something went wrong\" in msg\n for msg in log_messages), \"EXPLORE log not found\"\n # REFLECT should not be logged on failure\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n" + "body": "# #region test_belief_scope_error_handling [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs EXPLORE on exception.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain EXPLORE marker.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n log_records = list(caplog.records)\n assert any(_marker(r) == \"REASON\" and \"FailingFunction\" in r.getMessage() for r in log_records), \"REASON log not found\"\n assert any(\n _marker(r) == \"EXPLORE\" and \"Something went wrong\" in (getattr(r, 'error', '') or '')\n for r in log_records\n ), \"EXPLORE log not found\"\n # REFLECT should not be logged on failure\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n" }, { "contract_id": "test_belief_scope_success_reflect", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 92, - "end_line": 113, + "start_line": 100, + "end_line": 121, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21801,14 +22674,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_belief_scope_success_reflect [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs REFLECT on success.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_reflect(caplog):\n \"\"\"Test that belief_scope logs REFLECT on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"SuccessFunction\"):\n pass\n log_messages = [record.message for record in caplog.records]\n assert any(\"[REFLECT]\" in msg and \"SuccessFunction\" in msg for msg in log_messages), \"REFLECT log not found\"\n\n# #endregion test_belief_scope_success_reflect\n" + "body": "# #region test_belief_scope_success_reflect [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope logs REFLECT on success.\n# @PRE belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_reflect(caplog):\n \"\"\"Test that belief_scope logs REFLECT on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n with belief_scope(\"SuccessFunction\"):\n pass\n log_records = list(caplog.records)\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT log not found\"\n\n# #endregion test_belief_scope_success_reflect\n" }, { "contract_id": "test_belief_scope_not_visible_at_info", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 114, - "end_line": 131, + "start_line": 122, + "end_line": 146, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21827,14 +22700,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_belief_scope_not_visible_at_info [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope REFLECT logs are NOT visible at INFO level.\n# @PRE belief_scope is available. caplog fixture is used.\n# @POST REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).\ndef test_belief_scope_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REFLECT logs are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n log_messages = [record.message for record in caplog.records]\n # Inline INFO log should be visible\n assert any(\"Doing something important\" in msg for msg in log_messages), \"INFO log not found\"\n # REASON uses info() → visible at INFO level\n assert any(\"[REASON]\" in msg and \"InfoLevelFunction\" in msg for msg in log_messages), \"REASON log should be visible at INFO\"\n # REFLECT uses debug() → NOT visible at INFO level\n assert not any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT log should not be visible at INFO\"\n# #endregion test_belief_scope_not_visible_at_info\n" + "body": "# #region test_belief_scope_not_visible_at_info [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that belief_scope REFLECT logs are NOT visible at INFO level.\n# @PRE belief_scope is available. caplog fixture is used.\n# @POST REASON is visible at INFO (uses info()); REFLECT is not (uses debug()).\ndef test_belief_scope_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REFLECT logs are NOT visible at INFO level.\n\n NOTE: Molecular CoT protocol: belief_scope uses cot_log(level='DEBUG')\n for both REASON (entry) and REFLECT (exit). At INFO level, neither\n should appear in caplog. The inline logger.info() IS visible because\n it goes through superset_tools_app at INFO.\n \"\"\"\n caplog.set_level(\"INFO\")\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n # Inline INFO log should be visible\n assert any(\"Doing something important\" in msg for msg in log_messages), \"INFO log not found\"\n # REASON uses DEBUG level in molecular CoT → NOT visible at INFO\n assert not any(_marker(r) == \"REASON\" for r in log_records), \"REASON is DEBUG-level, should NOT be visible at INFO\"\n # REFLECT uses DEBUG level → NOT visible at INFO level\n assert not any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT is DEBUG-level, should NOT be visible at INFO\"\n# #endregion test_belief_scope_not_visible_at_info\n" }, { "contract_id": "test_task_log_level_default", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 132, - "end_line": 141, + "start_line": 147, + "end_line": 156, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21859,8 +22732,8 @@ "contract_id": "test_should_log_task_level", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 142, - "end_line": 154, + "start_line": 157, + "end_line": 169, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21885,8 +22758,8 @@ "contract_id": "test_configure_logger_task_log_level", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 155, - "end_line": 171, + "start_line": 170, + "end_line": 186, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21911,8 +22784,8 @@ "contract_id": "test_enable_belief_state_flag", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 172, - "end_line": 198, + "start_line": 187, + "end_line": 214, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21931,14 +22804,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_enable_belief_state_flag [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that enable_belief_state flag controls belief_scope logging.\n# @PRE LoggingConfig is available. caplog fixture is used.\n# @POST belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n log_messages = [record.message for record in caplog.records]\n\n # \"Entering DisabledFunction\" (explicit logger.reason()) should NOT be logged when disabled\n assert not any(\"Entering\" in msg for msg in log_messages), \"REASON entry should not be logged when disabled\"\n # REFLECT should still be logged (internal tracking, not gated by _enable_belief_state)\n assert any(\"[REFLECT]\" in msg for msg in log_messages), \"REFLECT should still be logged\"\n# #endregion test_enable_belief_state_flag\n" + "body": "# #region test_enable_belief_state_flag [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test that enable_belief_state flag controls belief_scope logging.\n# @PRE LoggingConfig is available. caplog fixture is used.\n# @POST belief_scope explicit REASON logs are controlled by the flag; REFLECT always logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n log_records = list(caplog.records)\n log_messages = [record.message for record in log_records]\n\n # \"Entering DisabledFunction\" (explicit logger.reason()) should NOT be logged when disabled\n assert not any(\"Entering\" in msg for msg in log_messages), \"REASON entry should not be logged when disabled\"\n # REFLECT should still be logged (internal tracking, not gated by _enable_belief_state)\n assert any(_marker(r) == \"REFLECT\" for r in log_records), \"REFLECT should still be logged\"\n# #endregion test_enable_belief_state_flag\n" }, { "contract_id": "test_belief_scope_missing_anchor", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 199, - "end_line": 211, + "start_line": 215, + "end_line": 225, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21955,14 +22828,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_belief_scope_missing_anchor [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n\n from src.core.logger import belief_scope\n with pytest.raises(TypeError):\n # Missing required positional argument 'anchor_id'\n with belief_scope():\n pass\n# #endregion test_belief_scope_missing_anchor\n" + "body": "# #region test_belief_scope_missing_anchor [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @PRE condition: anchor_id must be provided\ndef test_belief_scope_missing_anchor():\n \"\"\"Test that belief_scope enforces anchor_id to be provided.\"\"\"\n import pytest\n\n from src.core.logger import belief_scope\n with pytest.raises(TypeError), belief_scope():\n pass\n# #endregion test_belief_scope_missing_anchor\n" }, { "contract_id": "test_configure_logger_post_conditions", "contract_type": "Function", "file_path": "backend/src/core/logger/__tests__/test_logger.py", - "start_line": 212, - "end_line": 249, + "start_line": 226, + "end_line": 264, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -21979,7 +22852,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_configure_logger_post_conditions [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n\n from src.core.config_models import LoggingConfig\n import src.core.logger as logger_module\n from src.core.logger import BeliefFormatter, configure_logger, get_task_log_level, logger\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to BeliefFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, BeliefFormatter)\n\n # 4. Global states\n assert logger_module._enable_belief_state is False\n assert get_task_log_level() == \"DEBUG\"\n# #endregion test_configure_logger_post_conditions\n" + "body": "# #region test_configure_logger_post_conditions [TYPE Function]\n# @RELATION BINDS_TO -> [test_logger]\n# @BRIEF Test @POST condition: Logger level, handlers, belief state flag, and task log level are updated.\ndef test_configure_logger_post_conditions(tmp_path):\n \"\"\"Test that configure_logger satisfies all @POST conditions.\"\"\"\n import logging\n from logging.handlers import RotatingFileHandler\n\n from src.core.config_models import LoggingConfig\n import src.core.logger as logger_module\n from src.core.logger import configure_logger, get_task_log_level, logger\n log_file = tmp_path / \"test.log\"\n config = LoggingConfig(\n level=\"WARNING\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False,\n file_path=str(log_file)\n )\n\n configure_logger(config)\n\n # 1. Logger level is updated\n assert logger.level == logging.WARNING\n\n # 2. Handlers are updated (file handler removed old ones, added new one)\n file_handlers = [h for h in logger.handlers if isinstance(h, RotatingFileHandler)]\n assert len(file_handlers) == 1\n import pathlib\n assert pathlib.Path(file_handlers[0].baseFilename) == log_file.resolve()\n\n # 3. Formatter is set to CotJsonFormatter (replaced BeliefFormatter)\n from src.core.logger import CotJsonFormatter\n for handler in logger.handlers:\n assert isinstance(handler.formatter, CotJsonFormatter)\n\n # 4. Global states\n assert logger_module._enable_belief_state is False\n assert get_task_log_level() == \"DEBUG\"\n# #endregion test_configure_logger_post_conditions\n" }, { "contract_id": "IdMappingServiceModule", @@ -23335,7 +24208,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region RateLimiterModule [C:4] [TYPE Module] [SEMANTICS rate_limit, security, throttle, auth]\n# @BRIEF Simple in-memory rate limiter for auth endpoints.\n# Tracks request frequency per IP and blocks excessive requests.\n# For production, replace with Redis-based limiter.\n# @LAYER Core\n# @INVARIANT Never blocks successful auth — only repeated failures.\n# Window resets after ATTEMPT_WINDOW seconds of no activity.\n# @REJECTED Redis-backed limiter — not available in all deployments.\n# Per-user limiting — user isn't known until after auth.\nimport time\nimport threading\nfrom collections import defaultdict\n\n# ── Configuration ──\nMAX_ATTEMPTS: int = 10 # Max failed attempts per window\nATTEMPT_WINDOW: int = 300 # Window in seconds (5 min)\nBAN_DURATION: int = 900 # Ban duration after limit reached (15 min)\n\n\nclass RateLimiter:\n \"\"\"In-memory sliding-window rate limiter keyed by IP address.\n\n Thread-safe. Tracks failed attempts. After MAX_ATTEMPTS failures within\n ATTEMPT_WINDOW seconds, the IP is banned for BAN_DURATION seconds.\n \"\"\"\n\n def __init__(self):\n self._lock = threading.Lock()\n # ip -> [timestamp, ...] sorted list of attempt timestamps\n self._attempts: dict[str, list[float]] = defaultdict(list)\n # ip -> ban_expires_at\n self._bans: dict[str, float] = {}\n\n # #region is_banned [TYPE Function]\n # @BRIEF Check if IP is currently banned.\n # @PRE ip is a valid IP string.\n # @POST Returns True if IP is banned and ban hasn't expired.\n def is_banned(self, ip: str) -> bool:\n with self._lock:\n expires = self._bans.get(ip)\n if expires is None:\n return False\n if time.monotonic() > expires:\n del self._bans[ip]\n return False\n return True\n # #endregion is_banned\n\n # #region record_attempt [TYPE Function]\n # @BRIEF Record a failed attempt from an IP.\n # @PRE ip is a valid IP string.\n # @POST If MAX_ATTEMPTS exceeded within window, IP is banned.\n def record_attempt(self, ip: str) -> None:\n with self._lock:\n now = time.monotonic()\n window_start = now - ATTEMPT_WINDOW\n\n # Prune old attempts\n self._attempts[ip] = [t for t in self._attempts[ip] if t > window_start]\n self._attempts[ip].append(now)\n\n if len(self._attempts[ip]) > MAX_ATTEMPTS:\n self._bans[ip] = now + BAN_DURATION\n del self._attempts[ip]\n # #endregion record_attempt\n\n # #region record_success [TYPE Function]\n # @BRIEF Clear attempt history on successful auth.\n # @PRE ip is a valid IP string.\n # @POST Attempt history for IP is cleared.\n def record_success(self, ip: str) -> None:\n with self._lock:\n self._attempts.pop(ip, None)\n # #endregion record_success\n\n\n# Singleton\nrate_limiter = RateLimiter()\n# #endregion RateLimiterModule\n" + "body": "# #region RateLimiterModule [C:4] [TYPE Module] [SEMANTICS rate_limit, security, throttle, auth]\n# @BRIEF Simple in-memory rate limiter for auth endpoints.\n# Tracks request frequency per IP and blocks excessive requests.\n# For production, replace with Redis-based limiter.\n# @LAYER Core\n# @INVARIANT Never blocks successful auth — only repeated failures.\n# Window resets after ATTEMPT_WINDOW seconds of no activity.\n# @REJECTED Redis-backed limiter — not available in all deployments.\n# Per-user limiting — user isn't known until after auth.\nfrom collections import defaultdict\nimport threading\nimport time\n\n# ── Configuration ──\nMAX_ATTEMPTS: int = 10 # Max failed attempts per window\nATTEMPT_WINDOW: int = 300 # Window in seconds (5 min)\nBAN_DURATION: int = 900 # Ban duration after limit reached (15 min)\n\n\nclass RateLimiter:\n \"\"\"In-memory sliding-window rate limiter keyed by IP address.\n\n Thread-safe. Tracks failed attempts. After MAX_ATTEMPTS failures within\n ATTEMPT_WINDOW seconds, the IP is banned for BAN_DURATION seconds.\n \"\"\"\n\n def __init__(self):\n self._lock = threading.Lock()\n # ip -> [timestamp, ...] sorted list of attempt timestamps\n self._attempts: dict[str, list[float]] = defaultdict(list)\n # ip -> ban_expires_at\n self._bans: dict[str, float] = {}\n\n # #region is_banned [TYPE Function]\n # @BRIEF Check if IP is currently banned.\n # @PRE ip is a valid IP string.\n # @POST Returns True if IP is banned and ban hasn't expired.\n def is_banned(self, ip: str) -> bool:\n with self._lock:\n expires = self._bans.get(ip)\n if expires is None:\n return False\n if time.monotonic() > expires:\n del self._bans[ip]\n return False\n return True\n # #endregion is_banned\n\n # #region record_attempt [TYPE Function]\n # @BRIEF Record a failed attempt from an IP.\n # @PRE ip is a valid IP string.\n # @POST If MAX_ATTEMPTS exceeded within window, IP is banned.\n def record_attempt(self, ip: str) -> None:\n with self._lock:\n now = time.monotonic()\n window_start = now - ATTEMPT_WINDOW\n\n # Prune old attempts\n self._attempts[ip] = [t for t in self._attempts[ip] if t > window_start]\n self._attempts[ip].append(now)\n\n if len(self._attempts[ip]) > MAX_ATTEMPTS:\n self._bans[ip] = now + BAN_DURATION\n del self._attempts[ip]\n # #endregion record_attempt\n\n # #region record_success [TYPE Function]\n # @BRIEF Clear attempt history on successful auth.\n # @PRE ip is a valid IP string.\n # @POST Attempt history for IP is cleared.\n def record_success(self, ip: str) -> None:\n with self._lock:\n self._attempts.pop(ip, None)\n # #endregion record_success\n\n\n# Singleton\nrate_limiter = RateLimiter()\n# #endregion RateLimiterModule\n" }, { "contract_id": "is_banned", @@ -25469,7 +26342,7 @@ "contract_type": "Module", "file_path": "backend/src/core/superset_client/_layout_utils.py", "start_line": 1, - "end_line": 197, + "end_line": 196, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -25495,7 +26368,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LayoutUtils [C:2] [TYPE Module] [SEMANTICS superset, dashboard, layout, position, json]\n# @BRIEF Utility functions for manipulating Superset dashboard position_json layout.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:Python:json]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n# @RATIONALE Extracted from SupersetDashboardsWriteMixin to stay under INV_7 400 LOC.\n\nimport json\nimport re\nfrom typing import Any\n\n\n# #region parse_position_json [C:1] [TYPE Function]\n# @BRIEF Parse position_json from a dashboard API response (may be string or dict).\n# @PRE raw_position is a string, dict, or None.\n# @POST Returns a dict.\ndef parse_position_json(raw_position: Any) -> dict:\n if isinstance(raw_position, str):\n return json.loads(raw_position) if raw_position.strip() else {}\n if isinstance(raw_position, dict):\n return raw_position\n return {}\n# #endregion parse_position_json\n\n\n# #region _estimate_markdown_height [C:1] [TYPE Function]\n# @BRIEF Estimate grid height for a MARKDOWN element based on HTML content.\n# Counts block-level tags and newlines to estimate visual line count,\n# then converts to Superset grid units (1 unit ≈ 2 text lines + padding).\n# @PRE content is a string of HTML/Markdown.\n# @POST Returns int between 19 and 200 inclusive.\ndef _estimate_markdown_height(content: str) -> int:\n \"\"\"Estimate MARKDOWN element height (position JSON units).\n\n Model: total = CSS min-height(40px) + content pixel height.\n Content height = text_lines × line-height(22px) + container padding.\n 1 position JSON unit ≈ 8 rendered pixels.\n\n Reference: user's banner (5 text lines, padding:12) → height=21 → ~168px.\n \"\"\"\n if not content:\n return 19\n\n # Detect container padding (e.g. padding:12 → 24px vertical)\n pad_match = re.search(r'padding\\s*:\\s*(\\d+)', content)\n container_padding = int(pad_match.group(1)) * 2 if pad_match else 0\n\n # Strip HTML to get plain text\n plain_text = re.sub(r'<[^>]+>', '', content)\n plain_text = re.sub(r'\\s+', ' ', plain_text).strip()\n\n if not plain_text:\n return 19\n\n # Text lines (wrapping at ~40 chars/line at 12col width)\n text_lines = len(plain_text) // 40 + 1\n\n # Content height (px): line-height(~22px) × lines + container padding\n content_px = text_lines * 22 + container_padding\n\n # Total: CSS min-height(40px) + content\n total_px = 40 + content_px\n\n # Convert to position JSON units (÷8)\n height = max(19, total_px // 8)\n return min(200, height)\n# #endregion _estimate_markdown_height\n\n\n# #region _generate_banner_id [C:1] [TYPE Function]\n# @BRIEF Generate a deterministic row and markdown key for a banner chart.\ndef _generate_banner_id(chart_id: int) -> tuple[str, str]:\n row_key = f\"ROW-banner-{chart_id}\"\n md_key = f\"MARKDOWN-banner-{chart_id}\"\n return row_key, md_key\n# #endregion _generate_banner_id\n\n\n# #region insert_banner_markdown_at_top [C:2] [TYPE Function]\n# @BRIEF Insert a ROW + native MARKDOWN pair at the top of the dashboard grid.\n# Creates a ROW entry connected to GRID_ID, and a MARKDOWN child inside it.\n# Shifts all existing grid ROWs down by 2 rows.\n# @PRE position_json is a mutable dict. content is the HTML/markdown string.\n# chart_id is the database chart_id for key generation.\n# @POST position_json is mutated; ROW + MARKDOWN inserted at top of GRID.\ndef insert_banner_markdown_at_top(\n position_json: dict, chart_id: int, content: str\n) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n # Shift existing ROW y-positions down\n for key, value in list(position_json.items()):\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)):\n meta[\"y\"] = cur_y + 2\n\n # Add MARKDOWN entry with adaptive height\n height = _estimate_markdown_height(content)\n position_json[md_key] = {\n \"type\": \"MARKDOWN\",\n \"id\": md_key,\n \"children\": [],\n \"parents\": [\"ROOT_ID\", \"GRID_ID\", row_key],\n \"meta\": {\n \"code\": content,\n \"width\": 12,\n \"height\": height,\n \"y\": 0,\n },\n }\n\n # Add ROW entry connected to GRID_ID\n position_json[row_key] = {\n \"type\": \"ROW\",\n \"id\": row_key,\n \"children\": [md_key],\n \"parents\": [\"ROOT_ID\", \"GRID_ID\"],\n \"meta\": {\n \"background\": \"BACKGROUND_TRANSPARENT\",\n \"y\": 0,\n },\n }\n\n # Insert ROW at beginning of GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list):\n if row_key in grid_children:\n grid_children.remove(row_key)\n grid_children.insert(0, row_key)\n\n return position_json\n# #endregion insert_banner_markdown_at_top\n\n\n# #region update_banner_markdown_content [C:1] [TYPE Function]\n# @BRIEF Update the code content and adaptive height of an existing banner markdown element.\n# @PRE position_json has markdown_key. content is the new HTML/markdown string.\n# @POST position_json is mutated; markdown content and height updated.\ndef update_banner_markdown_content(\n position_json: dict, markdown_key: str, content: str\n) -> dict:\n if markdown_key in position_json:\n meta = position_json[markdown_key].get(\"meta\", {})\n if isinstance(meta, dict):\n meta[\"code\"] = content\n meta[\"height\"] = _estimate_markdown_height(content)\n return position_json\n# #endregion update_banner_markdown_content\n\n\n# #region remove_banner_from_position [C:2] [TYPE Function]\n# @BRIEF Remove the banner ROW + MARKDOWN pair from the position dict and GRID children.\n# Shifts remaining items up by 2 if the banner was at y=0.\n# @PRE position_json is a mutable dict. chart_id identifies the banner.\n# @POST position_json is mutated; ROW and MARKDOWN entries removed.\ndef remove_banner_from_position(position_json: dict, chart_id: int) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n removed_y = None\n # Remove MARKDOWN entry\n if md_key in position_json:\n markdown_data = position_json[md_key]\n if isinstance(markdown_data, dict):\n meta = markdown_data.get(\"meta\", {})\n if isinstance(meta, dict):\n removed_y = meta.get(\"y\", 0)\n del position_json[md_key]\n\n # Remove ROW entry\n if row_key in position_json:\n del position_json[row_key]\n\n # Remove ROW from GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list) and row_key in grid_children:\n grid_children.remove(row_key)\n\n # Shift remaining items up\n if removed_y is not None and removed_y == 0:\n for key, value in position_json.items():\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)) and cur_y > 0:\n meta[\"y\"] = cur_y - 2\n return position_json\n# #endregion remove_banner_from_position\n\n# #endregion LayoutUtils\n" + "body": "# #region LayoutUtils [C:2] [TYPE Module] [SEMANTICS superset, dashboard, layout, position, json]\n# @BRIEF Utility functions for manipulating Superset dashboard position_json layout.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:Python:json]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n# @RATIONALE Extracted from SupersetDashboardsWriteMixin to stay under INV_7 400 LOC.\n\nimport json\nimport re\nfrom typing import Any\n\n\n# #region parse_position_json [C:1] [TYPE Function]\n# @BRIEF Parse position_json from a dashboard API response (may be string or dict).\n# @PRE raw_position is a string, dict, or None.\n# @POST Returns a dict.\ndef parse_position_json(raw_position: Any) -> dict:\n if isinstance(raw_position, str):\n return json.loads(raw_position) if raw_position.strip() else {}\n if isinstance(raw_position, dict):\n return raw_position\n return {}\n# #endregion parse_position_json\n\n\n# #region _estimate_markdown_height [C:1] [TYPE Function]\n# @BRIEF Estimate grid height for a MARKDOWN element based on HTML content.\n# Counts block-level tags and newlines to estimate visual line count,\n# then converts to Superset grid units (1 unit ≈ 2 text lines + padding).\n# @PRE content is a string of HTML/Markdown.\n# @POST Returns int between 19 and 200 inclusive.\ndef _estimate_markdown_height(content: str) -> int:\n \"\"\"Estimate MARKDOWN element height (position JSON units).\n\n Model: total = CSS min-height(40px) + content pixel height.\n Content height = text_lines × line-height(22px) + container padding.\n 1 position JSON unit ≈ 8 rendered pixels.\n\n Reference: user's banner (5 text lines, padding:12) → height=21 → ~168px.\n \"\"\"\n if not content:\n return 19\n\n # Detect container padding (e.g. padding:12 → 24px vertical)\n pad_match = re.search(r'padding\\s*:\\s*(\\d+)', content)\n container_padding = int(pad_match.group(1)) * 2 if pad_match else 0\n\n # Strip HTML to get plain text\n plain_text = re.sub(r'<[^>]+>', '', content)\n plain_text = re.sub(r'\\s+', ' ', plain_text).strip()\n\n if not plain_text:\n return 19\n\n # Text lines (wrapping at ~40 chars/line at 12col width)\n text_lines = len(plain_text) // 40 + 1\n\n # Content height (px): line-height(~22px) × lines + container padding\n content_px = text_lines * 22 + container_padding\n\n # Total: CSS min-height(40px) + content\n total_px = 40 + content_px\n\n # Convert to position JSON units (÷8)\n height = max(19, total_px // 8)\n return min(200, height)\n# #endregion _estimate_markdown_height\n\n\n# #region _generate_banner_id [C:1] [TYPE Function]\n# @BRIEF Generate a deterministic row and markdown key for a banner chart.\ndef _generate_banner_id(chart_id: int) -> tuple[str, str]:\n row_key = f\"ROW-banner-{chart_id}\"\n md_key = f\"MARKDOWN-banner-{chart_id}\"\n return row_key, md_key\n# #endregion _generate_banner_id\n\n\n# #region insert_banner_markdown_at_top [C:2] [TYPE Function]\n# @BRIEF Insert a ROW + native MARKDOWN pair at the top of the dashboard grid.\n# Creates a ROW entry connected to GRID_ID, and a MARKDOWN child inside it.\n# Shifts all existing grid ROWs down by 2 rows.\n# @PRE position_json is a mutable dict. content is the HTML/markdown string.\n# chart_id is the database chart_id for key generation.\n# @POST position_json is mutated; ROW + MARKDOWN inserted at top of GRID.\ndef insert_banner_markdown_at_top(\n position_json: dict, chart_id: int, content: str\n) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n # Shift existing ROW y-positions down\n for key, value in list(position_json.items()):\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)):\n meta[\"y\"] = cur_y + 2\n\n # Add MARKDOWN entry with adaptive height\n height = _estimate_markdown_height(content)\n position_json[md_key] = {\n \"type\": \"MARKDOWN\",\n \"id\": md_key,\n \"children\": [],\n \"parents\": [\"ROOT_ID\", \"GRID_ID\", row_key],\n \"meta\": {\n \"code\": content,\n \"width\": 12,\n \"height\": height,\n \"y\": 0,\n },\n }\n\n # Add ROW entry connected to GRID_ID\n position_json[row_key] = {\n \"type\": \"ROW\",\n \"id\": row_key,\n \"children\": [md_key],\n \"parents\": [\"ROOT_ID\", \"GRID_ID\"],\n \"meta\": {\n \"background\": \"BACKGROUND_TRANSPARENT\",\n \"y\": 0,\n },\n }\n\n # Insert ROW at beginning of GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list):\n if row_key in grid_children:\n grid_children.remove(row_key)\n grid_children.insert(0, row_key)\n\n return position_json\n# #endregion insert_banner_markdown_at_top\n\n\n# #region update_banner_markdown_content [C:1] [TYPE Function]\n# @BRIEF Update the code content and adaptive height of an existing banner markdown element.\n# @PRE position_json has markdown_key. content is the new HTML/markdown string.\n# @POST position_json is mutated; markdown content and height updated.\ndef update_banner_markdown_content(\n position_json: dict, markdown_key: str, content: str\n) -> dict:\n if markdown_key in position_json:\n meta = position_json[markdown_key].get(\"meta\", {})\n if isinstance(meta, dict):\n meta[\"code\"] = content\n meta[\"height\"] = _estimate_markdown_height(content)\n return position_json\n# #endregion update_banner_markdown_content\n\n\n# #region remove_banner_from_position [C:2] [TYPE Function]\n# @BRIEF Remove the banner ROW + MARKDOWN pair from the position dict and GRID children.\n# Shifts remaining items up by 2 if the banner was at y=0.\n# @PRE position_json is a mutable dict. chart_id identifies the banner.\n# @POST position_json is mutated; ROW and MARKDOWN entries removed.\ndef remove_banner_from_position(position_json: dict, chart_id: int) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n removed_y = None\n # Remove MARKDOWN entry\n if md_key in position_json:\n markdown_data = position_json[md_key]\n if isinstance(markdown_data, dict):\n meta = markdown_data.get(\"meta\", {})\n if isinstance(meta, dict):\n removed_y = meta.get(\"y\", 0)\n del position_json[md_key]\n\n # Remove ROW entry\n position_json.pop(row_key, None)\n\n # Remove ROW from GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list) and row_key in grid_children:\n grid_children.remove(row_key)\n\n # Shift remaining items up\n if removed_y is not None and removed_y == 0:\n for key, value in position_json.items():\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)) and cur_y > 0:\n meta[\"y\"] = cur_y - 2\n return position_json\n# #endregion remove_banner_from_position\n\n# #endregion LayoutUtils\n" }, { "contract_id": "parse_position_json", @@ -25596,7 +26469,7 @@ "contract_type": "Function", "file_path": "backend/src/core/superset_client/_layout_utils.py", "start_line": 156, - "end_line": 195, + "end_line": 194, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -25607,7 +26480,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region remove_banner_from_position [C:2] [TYPE Function]\n# @BRIEF Remove the banner ROW + MARKDOWN pair from the position dict and GRID children.\n# Shifts remaining items up by 2 if the banner was at y=0.\n# @PRE position_json is a mutable dict. chart_id identifies the banner.\n# @POST position_json is mutated; ROW and MARKDOWN entries removed.\ndef remove_banner_from_position(position_json: dict, chart_id: int) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n removed_y = None\n # Remove MARKDOWN entry\n if md_key in position_json:\n markdown_data = position_json[md_key]\n if isinstance(markdown_data, dict):\n meta = markdown_data.get(\"meta\", {})\n if isinstance(meta, dict):\n removed_y = meta.get(\"y\", 0)\n del position_json[md_key]\n\n # Remove ROW entry\n if row_key in position_json:\n del position_json[row_key]\n\n # Remove ROW from GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list) and row_key in grid_children:\n grid_children.remove(row_key)\n\n # Shift remaining items up\n if removed_y is not None and removed_y == 0:\n for key, value in position_json.items():\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)) and cur_y > 0:\n meta[\"y\"] = cur_y - 2\n return position_json\n# #endregion remove_banner_from_position\n" + "body": "# #region remove_banner_from_position [C:2] [TYPE Function]\n# @BRIEF Remove the banner ROW + MARKDOWN pair from the position dict and GRID children.\n# Shifts remaining items up by 2 if the banner was at y=0.\n# @PRE position_json is a mutable dict. chart_id identifies the banner.\n# @POST position_json is mutated; ROW and MARKDOWN entries removed.\ndef remove_banner_from_position(position_json: dict, chart_id: int) -> dict:\n row_key, md_key = _generate_banner_id(chart_id)\n\n removed_y = None\n # Remove MARKDOWN entry\n if md_key in position_json:\n markdown_data = position_json[md_key]\n if isinstance(markdown_data, dict):\n meta = markdown_data.get(\"meta\", {})\n if isinstance(meta, dict):\n removed_y = meta.get(\"y\", 0)\n del position_json[md_key]\n\n # Remove ROW entry\n position_json.pop(row_key, None)\n\n # Remove ROW from GRID_ID children\n grid = position_json.get(\"GRID_ID\")\n if grid and isinstance(grid, dict):\n grid_children = grid.get(\"children\", [])\n if isinstance(grid_children, list) and row_key in grid_children:\n grid_children.remove(row_key)\n\n # Shift remaining items up\n if removed_y is not None and removed_y == 0:\n for key, value in position_json.items():\n if isinstance(value, dict):\n meta = value.get(\"meta\", {})\n if isinstance(meta, dict):\n cur_y = meta.get(\"y\", 0)\n if isinstance(cur_y, (int, float)) and cur_y > 0:\n meta[\"y\"] = cur_y - 2\n return position_json\n# #endregion remove_banner_from_position\n" }, { "contract_id": "SupersetUserProjection", @@ -26647,7 +27520,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region JobLifecycleModule [C:5] [TYPE Module] [SEMANTICS task,lifecycle,state,machine,execution]\n# @BRIEF Task creation, execution, pause/resume, and completion transitions for plugin-backed\n# jobs — extracted from the monolithic TaskManager.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RATIONALE Extracted from TaskManager to satisfy INV_7. JobLifecycle encodes the task state\n# machine with plugin execution, context injection, backward-compatible plugin\n# dispatch, and input/mapping pause/resume transitions.\n# @REJECTED Keeping lifecycle/execution logic inside TaskManager was rejected — it mixes\n# async execution orchestration with passive registry and log buffering concerns,\n# violating single-responsibility and INV_7.\n# @PRE Requested plugin ids resolve to executable plugins; task state transitions target known\n# TaskStatus values.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @SIDE_EFFECT Schedules async execution, pauses on input/mapping requests, mutates persisted\n# task lifecycle markers.\n# @INVARIANT A task cannot be resumed from a waiting state unless a matching future exists or\n# a new wait future is created.\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import UTC, datetime\nimport inspect\nfrom typing import Any\n\nfrom ..cot_logger import seed_trace_id\nfrom ..logger import belief_scope, logger\nfrom .context import TaskContext\nfrom .graph import TaskGraph\nfrom .models import Task, TaskStatus\n\n\n# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n self._dataset_subscribers: dict[str, list[asyncio.Queue]] = {}\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # Broadcast dataset.updated for mapping and documentation tasks (FR-024, R4)\n if task.plugin_id in (\"dataset-mapper\", \"llm_documentation\"):\n dataset_ids = task.params.get(\"dataset_ids\") or [task.params.get(\"dataset_id\")]\n env_id = task.params.get(\"env\") or task.params.get(\"environment_id\", \"\")\n if dataset_ids and env_id:\n self._broadcast_dataset_updated(env_id, dataset_ids)\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n\n # #region subscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,subscribe,events,websocket]\n # @BRIEF Subscribe to dataset.updated events for a specific environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if env_id not in self._dataset_subscribers:\n self._dataset_subscribers[env_id] = []\n self._dataset_subscribers[env_id].append(queue)\n return queue\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,unsubscribe,events]\n # @BRIEF Unsubscribe from dataset.updated events for a specific environment.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n if env_id in self._dataset_subscribers:\n if queue in self._dataset_subscribers[env_id]:\n self._dataset_subscribers[env_id].remove(queue)\n if not self._dataset_subscribers[env_id]:\n del self._dataset_subscribers[env_id]\n # #endregion unsubscribe_dataset_events\n\n # #region _broadcast_dataset_updated [C:3] [TYPE Function] [SEMANTICS dataset,broadcast,event,websocket]\n # @BRIEF Broadcast dataset.updated event to all subscribers for a given environment.\n def _broadcast_dataset_updated(self, env_id: str, dataset_ids: list[int]):\n event = {\"type\": \"dataset.updated\", \"payload\": {\"env_id\": env_id, \"dataset_ids\": dataset_ids}}\n if env_id in self._dataset_subscribers:\n for queue in self._dataset_subscribers[env_id]:\n try:\n self.loop.call_soon_threadsafe(queue.put_nowait, event)\n except Exception:\n pass\n # #endregion _broadcast_dataset_updated\n# #endregion JobLifecycle\n# #endregion JobLifecycleModule\n" + "body": "# #region JobLifecycleModule [C:5] [TYPE Module] [SEMANTICS task,lifecycle,state,machine,execution]\n# @BRIEF Task creation, execution, pause/resume, and completion transitions for plugin-backed\n# jobs — extracted from the monolithic TaskManager.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RATIONALE Extracted from TaskManager to satisfy INV_7. JobLifecycle encodes the task state\n# machine with plugin execution, context injection, backward-compatible plugin\n# dispatch, and input/mapping pause/resume transitions.\n# @REJECTED Keeping lifecycle/execution logic inside TaskManager was rejected — it mixes\n# async execution orchestration with passive registry and log buffering concerns,\n# violating single-responsibility and INV_7.\n# @PRE Requested plugin ids resolve to executable plugins; task state transitions target known\n# TaskStatus values.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @SIDE_EFFECT Schedules async execution, pauses on input/mapping requests, mutates persisted\n# task lifecycle markers.\n# @INVARIANT A task cannot be resumed from a waiting state unless a matching future exists or\n# a new wait future is created.\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom datetime import UTC, datetime\nimport inspect\nfrom typing import Any\n\nfrom ..cot_logger import seed_trace_id\nfrom ..logger import belief_scope, logger\nfrom .context import TaskContext\nfrom .graph import TaskGraph\nfrom .models import Task, TaskStatus\n\n\n# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n self._dataset_subscribers: dict[str, list[asyncio.Queue]] = {}\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.now(UTC)\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.now(UTC)\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # Broadcast dataset.updated for mapping and documentation tasks (FR-024, R4)\n if task.plugin_id in (\"dataset-mapper\", \"llm_documentation\"):\n dataset_ids = task.params.get(\"dataset_ids\") or [task.params.get(\"dataset_id\")]\n env_id = task.params.get(\"env\") or task.params.get(\"environment_id\", \"\")\n if dataset_ids and env_id:\n self._broadcast_dataset_updated(env_id, dataset_ids)\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n\n # #region subscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,subscribe,events,websocket]\n # @BRIEF Subscribe to dataset.updated events for a specific environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if env_id not in self._dataset_subscribers:\n self._dataset_subscribers[env_id] = []\n self._dataset_subscribers[env_id].append(queue)\n return queue\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,unsubscribe,events]\n # @BRIEF Unsubscribe from dataset.updated events for a specific environment.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n if env_id in self._dataset_subscribers:\n if queue in self._dataset_subscribers[env_id]:\n self._dataset_subscribers[env_id].remove(queue)\n if not self._dataset_subscribers[env_id]:\n del self._dataset_subscribers[env_id]\n # #endregion unsubscribe_dataset_events\n\n # #region _broadcast_dataset_updated [C:3] [TYPE Function] [SEMANTICS dataset,broadcast,event,websocket]\n # @BRIEF Broadcast dataset.updated event to all subscribers for a given environment.\n def _broadcast_dataset_updated(self, env_id: str, dataset_ids: list[int]):\n event = {\"type\": \"dataset.updated\", \"payload\": {\"env_id\": env_id, \"dataset_ids\": dataset_ids}}\n if env_id in self._dataset_subscribers:\n for queue in self._dataset_subscribers[env_id]:\n try:\n self.loop.call_soon_threadsafe(queue.put_nowait, event)\n except Exception:\n pass\n # #endregion _broadcast_dataset_updated\n# #endregion JobLifecycle\n# #endregion JobLifecycleModule\n" }, { "contract_id": "JobLifecycle", @@ -26665,7 +27538,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n self._dataset_subscribers: dict[str, list[asyncio.Queue]] = {}\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.utcnow()\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.utcnow()\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # Broadcast dataset.updated for mapping and documentation tasks (FR-024, R4)\n if task.plugin_id in (\"dataset-mapper\", \"llm_documentation\"):\n dataset_ids = task.params.get(\"dataset_ids\") or [task.params.get(\"dataset_id\")]\n env_id = task.params.get(\"env\") or task.params.get(\"environment_id\", \"\")\n if dataset_ids and env_id:\n self._broadcast_dataset_updated(env_id, dataset_ids)\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n\n # #region subscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,subscribe,events,websocket]\n # @BRIEF Subscribe to dataset.updated events for a specific environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if env_id not in self._dataset_subscribers:\n self._dataset_subscribers[env_id] = []\n self._dataset_subscribers[env_id].append(queue)\n return queue\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,unsubscribe,events]\n # @BRIEF Unsubscribe from dataset.updated events for a specific environment.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n if env_id in self._dataset_subscribers:\n if queue in self._dataset_subscribers[env_id]:\n self._dataset_subscribers[env_id].remove(queue)\n if not self._dataset_subscribers[env_id]:\n del self._dataset_subscribers[env_id]\n # #endregion unsubscribe_dataset_events\n\n # #region _broadcast_dataset_updated [C:3] [TYPE Function] [SEMANTICS dataset,broadcast,event,websocket]\n # @BRIEF Broadcast dataset.updated event to all subscribers for a given environment.\n def _broadcast_dataset_updated(self, env_id: str, dataset_ids: list[int]):\n event = {\"type\": \"dataset.updated\", \"payload\": {\"env_id\": env_id, \"dataset_ids\": dataset_ids}}\n if env_id in self._dataset_subscribers:\n for queue in self._dataset_subscribers[env_id]:\n try:\n self.loop.call_soon_threadsafe(queue.put_nowait, event)\n except Exception:\n pass\n # #endregion _broadcast_dataset_updated\n# #endregion JobLifecycle\n" + "body": "# #region JobLifecycle [C:5] [TYPE Class] [SEMANTICS task,lifecycle,execution,state,machine]\n# @BRIEF Encodes task creation, execution, pause/resume, and completion transitions for\n# plugin-backed jobs.\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @PRE plugin_loader resolves plugin ids; graph and event_bus are initialized.\n# @POST Every scheduled task transitions through a valid lifecycle path ending in persisted\n# terminal or waiting state.\n# @RATIONALE Extracted as standalone lifecycle state machine to isolate async execution\n# orchestration (plugin dispatch, context injection, pause/resume futures) from\n# passive registry and log buffering concerns.\n# @REJECTED Keeping execution logic inside TaskManager was rejected — it mixed async task\n# orchestration with synchronous registry operations and background threading,\n# violating single-responsibility and making the 708-line module unmaintainable.\nclass JobLifecycle:\n \"\"\"Task state machine: creation, execution, pause/resume, completion.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with dependencies.\n def __init__(\n self,\n plugin_loader: Any,\n graph: TaskGraph,\n event_bus: Any,\n persistence_service: Any,\n executor: ThreadPoolExecutor,\n loop: asyncio.AbstractEventLoop,\n ):\n self.plugin_loader = plugin_loader\n self.graph = graph\n self.event_bus = event_bus\n self.persistence_service = persistence_service\n self.executor = executor\n self.loop = loop\n self._dataset_subscribers: dict[str, list[asyncio.Queue]] = {}\n # #endregion __init__\n\n # #region create_task [C:4] [TYPE Function] [SEMANTICS task,create,persist,schedule]\n # @BRIEF Creates and queues a new task for execution.\n # @PRE Plugin with plugin_id exists. Params are valid dict.\n # @POST Task is created, added to registry, and scheduled for execution.\n # @RAISES ValueError if plugin not found or params invalid.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None,\n add_log_callback=None,\n ) -> Task:\n with belief_scope(\"JobLifecycle.create_task\", f\"plugin_id={plugin_id}\"):\n if not self.plugin_loader.has_plugin(plugin_id):\n logger.error(f\"Plugin with ID '{plugin_id}' not found.\")\n raise ValueError(f\"Plugin with ID '{plugin_id}' not found.\")\n self.plugin_loader.get_plugin(plugin_id)\n if not isinstance(params, dict):\n logger.error(\"Task parameters must be a dictionary.\")\n raise ValueError(\"Task parameters must be a dictionary.\")\n logger.reason(\"Creating task node and scheduling execution\")\n task = Task(plugin_id=plugin_id, params=params, user_id=user_id)\n self.graph.add_task(task)\n self.persistence_service.persist_task(task)\n logger.info(f\"Task {task.id} created and scheduled for execution\")\n self.loop.create_task(\n self._run_task(task.id, add_log_callback=add_log_callback)\n )\n logger.reflect(\n \"Task creation persisted and execution scheduled\",\n extra={\"task_id\": task.id, \"plugin_id\": plugin_id},\n )\n return task\n # #endregion create_task\n\n # #region _run_task [C:4] [TYPE Function] [SEMANTICS task,execute,context,dispatch]\n # @BRIEF Internal method to execute a task with TaskContext support.\n # @PRE Task exists in registry.\n # @POST Task is executed, status updated to SUCCESS or FAILED.\n # @SIDE_EFFECT Executes plugin code, updates task status and persistence, emits logs.\n async def _run_task(self, task_id: str, add_log_callback=None):\n seed_trace_id()\n with belief_scope(\"JobLifecycle._run_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if task is None:\n logger.error(f\"Task {task_id} not found in registry\")\n return\n plugin = self.plugin_loader.get_plugin(task.plugin_id)\n logger.reason(\n \"Transitioning task to running state\",\n extra={\"task_id\": task_id, \"plugin_id\": task.plugin_id},\n )\n logger.info(\n f\"Starting execution of task {task_id} for plugin '{plugin.name}'\"\n )\n task.status = TaskStatus.RUNNING\n task.started_at = datetime.now(UTC)\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task started for plugin '{plugin.name}'\",\n source=\"system\",\n )\n try:\n params = {**task.params, \"_task_id\": task_id}\n sig = inspect.signature(plugin.execute)\n accepts_context = \"context\" in sig.parameters\n if accepts_context:\n # Create TaskContext for new-style plugins\n context = TaskContext(\n task_id=task_id,\n add_log_fn=add_log_callback,\n params=params,\n default_source=\"plugin\",\n background_tasks=None,\n )\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params, context=context)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor,\n lambda: plugin.execute(params, context=context),\n )\n else:\n if asyncio.iscoroutinefunction(plugin.execute):\n task.result = await plugin.execute(params)\n else:\n task.result = await self.loop.run_in_executor(\n self.executor, plugin.execute, params\n )\n logger.info(f\"Task {task_id} completed successfully\")\n task.status = TaskStatus.SUCCESS\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n f\"Task completed successfully for plugin '{plugin.name}'\",\n source=\"system\",\n )\n except Exception as e:\n logger.error(f\"Task {task_id} failed: {e}\")\n task.status = TaskStatus.FAILED\n if add_log_callback:\n add_log_callback(\n task_id, \"ERROR\",\n f\"Task failed: {e}\",\n source=\"system\",\n metadata={\"error_type\": type(e).__name__},\n )\n finally:\n task.finished_at = datetime.now(UTC)\n self.event_bus.flush_task_logs(task_id)\n self.persistence_service.persist_task(task)\n logger.info(\n f\"Task {task_id} execution finished with status: {task.status}\"\n )\n logger.reflect(\n \"Task lifecycle reached persisted terminal state\",\n extra={\"task_id\": task_id, \"status\": str(task.status)},\n )\n # Broadcast dataset.updated for mapping and documentation tasks (FR-024, R4)\n if task.plugin_id in (\"dataset-mapper\", \"llm_documentation\"):\n dataset_ids = task.params.get(\"dataset_ids\") or [task.params.get(\"dataset_id\")]\n env_id = task.params.get(\"env\") or task.params.get(\"environment_id\", \"\")\n if dataset_ids and env_id:\n self._broadcast_dataset_updated(env_id, dataset_ids)\n # #endregion _run_task\n\n # #region resolve_task [C:3] [TYPE Function] [SEMANTICS task,resolve,mapping,resume]\n # @BRIEF Resumes a task that is awaiting mapping.\n # @PRE Task exists and is in AWAITING_MAPPING state.\n # @POST Task status updated to RUNNING, params updated, execution resumed.\n # @RAISES ValueError if task not found or not awaiting mapping.\n async def resolve_task(\n self, task_id: str, resolution_params: dict[str, Any],\n ) -> None:\n with belief_scope(\"JobLifecycle.resolve_task\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task or task.status != TaskStatus.AWAITING_MAPPING:\n raise ValueError(\"Task is not awaiting mapping.\")\n task.params.update(resolution_params)\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n self.graph.resolve_future(task_id, True)\n # #endregion resolve_task\n\n # #region wait_for_resolution [C:3] [TYPE Function] [SEMANTICS task,wait,mapping,future]\n # @BRIEF Pauses execution and waits for a resolution signal.\n # @PRE Task exists.\n # @POST Execution pauses until future is set.\n async def wait_for_resolution(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_resolution\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n task.status = TaskStatus.AWAITING_MAPPING\n self.persistence_service.persist_task(task)\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [C:3] [TYPE Function] [SEMANTICS task,wait,input,future]\n # @BRIEF Pauses execution and waits for user input.\n # @PRE Task exists.\n # @POST Execution pauses until future is set via resume_task_with_password.\n async def wait_for_input(self, task_id: str) -> None:\n with belief_scope(\"JobLifecycle.wait_for_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n return\n future = self.loop.create_future()\n self.graph.create_future(task_id, future)\n try:\n await future\n finally:\n self.graph.remove_future(task_id)\n # #endregion wait_for_input\n\n # #region await_input [C:3] [TYPE Function] [SEMANTICS task,input,pause,state]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n # @PRE Task exists and is in RUNNING state.\n # @POST Task status changed to AWAITING_INPUT, input_request set, persisted.\n # @RAISES ValueError if task not found or not RUNNING.\n def await_input(\n self, task_id: str, input_request: dict[str, Any],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\"JobLifecycle.await_input\", f\"task_id={task_id}\"):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.RUNNING:\n raise ValueError(\n f\"Task {task_id} is not RUNNING (current: {task.status})\"\n )\n task.status = TaskStatus.AWAITING_INPUT\n task.input_required = True\n task.input_request = input_request\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task paused for user input\",\n metadata={\"input_request\": input_request},\n )\n # #endregion await_input\n\n # #region resume_task_with_password [C:3] [TYPE Function] [SEMANTICS task,resume,password,input]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n # @PRE Task exists and is in AWAITING_INPUT state.\n # @POST Task status changed to RUNNING, passwords injected, task resumed.\n # @RAISES ValueError if task not found, not awaiting input, or passwords invalid.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str],\n add_log_callback=None,\n ) -> None:\n with belief_scope(\n \"JobLifecycle.resume_task_with_password\", f\"task_id={task_id}\"\n ):\n task = self.graph.get_task(task_id)\n if not task:\n raise ValueError(f\"Task {task_id} not found\")\n if task.status != TaskStatus.AWAITING_INPUT:\n raise ValueError(\n f\"Task {task_id} is not AWAITING_INPUT (current: {task.status})\"\n )\n if not isinstance(passwords, dict) or not passwords:\n raise ValueError(\"Passwords must be a non-empty dictionary\")\n task.params[\"passwords\"] = passwords\n task.input_required = False\n task.input_request = None\n task.status = TaskStatus.RUNNING\n self.persistence_service.persist_task(task)\n if add_log_callback:\n add_log_callback(\n task_id, \"INFO\",\n \"Task resumed with passwords\",\n metadata={\"databases\": list(passwords.keys())},\n )\n self.graph.resolve_future(task_id, True)\n # #endregion resume_task_with_password\n\n # #region subscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,subscribe,events,websocket]\n # @BRIEF Subscribe to dataset.updated events for a specific environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n queue = asyncio.Queue()\n if env_id not in self._dataset_subscribers:\n self._dataset_subscribers[env_id] = []\n self._dataset_subscribers[env_id].append(queue)\n return queue\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [C:2] [TYPE Function] [SEMANTICS dataset,unsubscribe,events]\n # @BRIEF Unsubscribe from dataset.updated events for a specific environment.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n if env_id in self._dataset_subscribers:\n if queue in self._dataset_subscribers[env_id]:\n self._dataset_subscribers[env_id].remove(queue)\n if not self._dataset_subscribers[env_id]:\n del self._dataset_subscribers[env_id]\n # #endregion unsubscribe_dataset_events\n\n # #region _broadcast_dataset_updated [C:3] [TYPE Function] [SEMANTICS dataset,broadcast,event,websocket]\n # @BRIEF Broadcast dataset.updated event to all subscribers for a given environment.\n def _broadcast_dataset_updated(self, env_id: str, dataset_ids: list[int]):\n event = {\"type\": \"dataset.updated\", \"payload\": {\"env_id\": env_id, \"dataset_ids\": dataset_ids}}\n if env_id in self._dataset_subscribers:\n for queue in self._dataset_subscribers[env_id]:\n try:\n self.loop.call_soon_threadsafe(queue.put_nowait, event)\n except Exception:\n pass\n # #endregion _broadcast_dataset_updated\n# #endregion JobLifecycle\n" }, { "contract_id": "_broadcast_dataset_updated", @@ -26690,7 +27563,7 @@ "contract_type": "Module", "file_path": "backend/src/core/task_manager/manager.py", "start_line": 1, - "end_line": 336, + "end_line": 335, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -26701,14 +27574,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskManagerModule [C:5] [TYPE Module] [SEMANTICS task, schedule, execution, task-manager]\n# @BRIEF Thin facade composing TaskGraph (registry), EventBus (log/pub-sub), and JobLifecycle\n# (state machine) into a single TaskManager interface for backward compatibility.\n# @LAYER Core\n# @PRE Plugin loader and database sessions are initialized.\n# @POST Orchestrates task execution and persistence.\n# @SIDE_EFFECT Spawns worker threads and flushes logs to DB.\n# @DATA_CONTRACT Input[plugin_id, params] -> Model[Task, LogEntry]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT Task IDs are unique.\n# @TEST_CONTRACT TaskManagerRuntime -> {\n# required_fields: {plugin_loader: PluginLoader},\n# optional_fields: {},\n# invariants: [\"Must use belief_scope for logging\"]\n# }\n# @TEST_FIXTURE valid_module -> {\"manager_initialized\": true}\n# @TEST_EDGE missing_required_field -> {\"plugin_loader\": null}\n# @TEST_EDGE empty_response -> {\"tasks\": []}\n# @TEST_EDGE invalid_type -> {\"plugin_loader\": \"string_instead_of_object\"}\n# @TEST_EDGE external_failure -> {\"db_unavailable\": true}\n# @TEST_INVARIANT logger_compliance -> verifies: [valid_module]\n# @RATIONALE Decomposed from 708-line monolithic module into four focused modules (TaskGraph,\n# EventBus, JobLifecycle, and this facade) to satisfy INV_7. TaskManager now delegates\n# to sub-services while preserving the public API contract.\n# @REJECTED Keeping all five concerns (registry, lifecycle, log buffer, subscriptions, execution)\n# in one module was rejected — it violated INV_7 (708 lines vs 400 max), mixed threading\n# with async execution, and created an unmaintainable god class.\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nimport threading\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\nfrom .event_bus import EventBus\nfrom .graph import TaskGraph\nfrom .lifecycle import JobLifecycle\nfrom .models import LogFilter, LogStats, Task, TaskStatus\nfrom .persistence import TaskLogPersistenceService, TaskPersistenceService\n\n\n# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Facade composing TaskGraph, EventBus, and JobLifecycle into a single interface.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE Plugin loader resolves plugin ids and persistence services are available.\n# @POST In-memory task graph, lifecycle scheduler, and log event bus stay consistent with\n# persisted task state.\n# @INVARIANT Task IDs are unique within the registry.\n# @INVARIANT Each task has exactly one status at any time.\n# @INVARIANT Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT Input[plugin_id, params] -> Output[Task]\n# @RATIONALE Thin facade — all delegating methods are under 15 lines. Actual business logic\n# (registry CRUD, log buffering, lifecycle state machine) lives in extracted modules.\n# @REJECTED Keeping all five concerns in one class was rejected — it violated INV_7 (708-line\n# manager.py) and made the god class impossible to test or maintain independently.\nclass TaskManager:\n \"\"\"\n Facade composing TaskGraph (registry), EventBus (log/pub-sub), and\n JobLifecycle (state machine) into a single TaskManager interface.\n \"\"\"\n\n # #region __init__ [TYPE Function] [C:5]\n # @BRIEF Initialize sub-services, create add_log callback, start background flusher.\n # @PRE plugin_loader is initialized.\n # @POST TaskManager is ready to accept tasks.\n # @SIDE_EFFECT Starts background flusher thread and loads persisted task state into memory.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.executor = ThreadPoolExecutor(max_workers=5)\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n\n # Create sub-services\n self.graph = TaskGraph(self.persistence_service)\n self.event_bus = EventBus(self.log_persistence_service, self.loop)\n\n # Create add_log callback that lifecycle and plugins use\n self._add_log = self._make_add_log_callback()\n\n self.lifecycle = JobLifecycle(\n plugin_loader=self.plugin_loader,\n graph=self.graph,\n event_bus=self.event_bus,\n persistence_service=self.persistence_service,\n executor=self.executor,\n loop=self.loop,\n )\n\n # Load persisted tasks on startup\n self.graph.load_persisted_tasks()\n\n # Backward-compatible property aliases for tests\n self.tasks = self.graph.tasks\n self.task_futures = self.graph.task_futures\n self.subscribers = self.event_bus.subscribers\n self._log_buffer = self.event_bus._log_buffer\n self._log_buffer_lock = self.event_bus._log_buffer_lock\n self._flusher_stop_event = self.event_bus._flusher_stop_event\n self._flusher_thread = self.event_bus._flusher_thread\n\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n\n # #region _make_add_log_callback [C:2] [TYPE Function]\n # @BRIEF Create a closure for adding logs that looks up the task and delegates to EventBus.\n def _make_add_log_callback(self):\n def _add_log(task_id, level, message, source=\"system\", metadata=None, context=None):\n task = self.graph.get_task(task_id)\n if not task:\n return\n self.event_bus.add_log(\n task_id, level, message, source, metadata, context,\n task_logs_list=task.logs,\n )\n return _add_log\n # #endregion _make_add_log_callback\n\n # ── Deprecated legacy aliases (delegate to EventBus) ──\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Legacy alias delegating to EventBus._flusher_loop.\n def _flusher_loop(self):\n self.event_bus._flusher_loop()\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Legacy alias delegating to EventBus._flush_logs.\n def _flush_logs(self):\n self.event_bus._flush_logs()\n # #endregion _flush_logs\n\n # #region _flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Legacy alias delegating to EventBus.flush_task_logs.\n def _flush_task_logs(self, task_id: str):\n self.event_bus.flush_task_logs(task_id)\n # #endregion _flush_task_logs\n\n # ── Task CRUD delegates to TaskGraph ──\n\n # #region get_task [TYPE Function] [C:2]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.graph.get_task(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [TYPE Function] [C:1]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return self.graph.get_all_tasks()\n # #endregion get_all_tasks\n\n # #region get_tasks [TYPE Function] [C:3]\n # @BRIEF Retrieves tasks with pagination and optional status filter.\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n return self.graph.get_tasks(limit, offset, status, plugin_ids, completed_only)\n # #endregion get_tasks\n\n # #region load_persisted_tasks [TYPE Function] [C:2]\n # @BRIEF Load persisted tasks using persistence service.\n def load_persisted_tasks(self) -> None:\n self.graph.load_persisted_tasks(limit=100)\n # #endregion load_persisted_tasks\n\n # #region clear_tasks [TYPE Function] [C:4]\n # @BRIEF Clears tasks based on status filter (also deletes associated logs).\n # @SIDE_EFFECT Removes tasks from registry and persistence; cancels futures.\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n\n # Delete logs first, then remove tasks\n if tasks_to_remove:\n self.event_bus.delete_logs_for_tasks(tasks_to_remove)\n removed = self.graph.remove_tasks(tasks_to_remove)\n logger.info(f\"Cleared {removed} tasks.\")\n return removed\n # #endregion clear_tasks\n\n # ── Log delegates to EventBus ──\n\n # #region get_task_logs [TYPE Function] [C:3]\n # @BRIEF Retrieves logs for a specific task (from memory or persistence).\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list:\n task = self.graph.get_task(task_id)\n task_status = task.status if task else None\n task_logs = task.logs if task else []\n return self.event_bus.get_task_logs(\n task_id, log_filter, task_status=task_status, task_logs=task_logs\n )\n # #endregion get_task_logs\n\n # #region get_task_log_stats [TYPE Function] [C:2]\n # @BRIEF Get statistics about logs for a task.\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.event_bus.get_task_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [TYPE Function] [C:2]\n # @BRIEF Get unique sources for a task's logs.\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.event_bus.get_task_log_sources(task_id)\n # #endregion get_task_log_sources\n\n # ── Subscription delegates to EventBus ──\n\n # #region subscribe_logs [TYPE Function] [C:2]\n # @BRIEF Subscribes to real-time logs for a task.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n return await self.event_bus.subscribe_logs(task_id)\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [TYPE Function] [C:2]\n # @BRIEF Unsubscribes from real-time logs for a task.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n self.event_bus.unsubscribe_logs(task_id, queue)\n # #endregion unsubscribe_logs\n\n # ── Lifecycle delegates to JobLifecycle ──\n\n # #region create_task [TYPE Function] [C:4]\n # @BRIEF Creates and queues a new task for execution.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n return await self.lifecycle.create_task(\n plugin_id, params, user_id,\n add_log_callback=self._add_log,\n )\n # #endregion create_task\n\n # #region _run_task [TYPE Function] [C:4]\n # @BRIEF Internal method to execute a task with TaskContext support (delegates to lifecycle).\n async def _run_task(self, task_id: str):\n await self.lifecycle._run_task(task_id, add_log_callback=self._add_log)\n # #endregion _run_task\n\n # #region resolve_task [TYPE Function] [C:3]\n # @BRIEF Resumes a task that is awaiting mapping.\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n await self.lifecycle.resolve_task(task_id, resolution_params)\n # #endregion resolve_task\n\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for a resolution signal.\n async def wait_for_resolution(self, task_id: str):\n await self.lifecycle.wait_for_resolution(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for user input.\n async def wait_for_input(self, task_id: str):\n await self.lifecycle.wait_for_input(task_id)\n # #endregion wait_for_input\n\n # #region await_input [TYPE Function] [C:3]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n self.lifecycle.await_input(\n task_id, input_request,\n add_log_callback=self._add_log,\n )\n # #endregion await_input\n\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n self.lifecycle.resume_task_with_password(\n task_id, passwords,\n add_log_callback=self._add_log,\n )\n # #endregion resume_task_with_password\n\n # ── Dataset event delegates to JobLifecycle ──\n\n # #region subscribe_dataset_events [TYPE Function] [C:2]\n # @BRIEF Subscribe to dataset.updated events for an environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n return await self.lifecycle.subscribe_dataset_events(env_id)\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [TYPE Function] [C:2]\n # @BRIEF Unsubscribe from dataset.updated events.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n self.lifecycle.unsubscribe_dataset_events(env_id, queue)\n # #endregion unsubscribe_dataset_events\n# #endregion TaskManager\n# #endregion TaskManagerModule\n" + "body": "# #region TaskManagerModule [C:5] [TYPE Module] [SEMANTICS task, schedule, execution, task-manager]\n# @BRIEF Thin facade composing TaskGraph (registry), EventBus (log/pub-sub), and JobLifecycle\n# (state machine) into a single TaskManager interface for backward compatibility.\n# @LAYER Core\n# @PRE Plugin loader and database sessions are initialized.\n# @POST Orchestrates task execution and persistence.\n# @SIDE_EFFECT Spawns worker threads and flushes logs to DB.\n# @DATA_CONTRACT Input[plugin_id, params] -> Model[Task, LogEntry]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT Task IDs are unique.\n# @TEST_CONTRACT TaskManagerRuntime -> {\n# required_fields: {plugin_loader: PluginLoader},\n# optional_fields: {},\n# invariants: [\"Must use belief_scope for logging\"]\n# }\n# @TEST_FIXTURE valid_module -> {\"manager_initialized\": true}\n# @TEST_EDGE missing_required_field -> {\"plugin_loader\": null}\n# @TEST_EDGE empty_response -> {\"tasks\": []}\n# @TEST_EDGE invalid_type -> {\"plugin_loader\": \"string_instead_of_object\"}\n# @TEST_EDGE external_failure -> {\"db_unavailable\": true}\n# @TEST_INVARIANT logger_compliance -> verifies: [valid_module]\n# @RATIONALE Decomposed from 708-line monolithic module into four focused modules (TaskGraph,\n# EventBus, JobLifecycle, and this facade) to satisfy INV_7. TaskManager now delegates\n# to sub-services while preserving the public API contract.\n# @REJECTED Keeping all five concerns (registry, lifecycle, log buffer, subscriptions, execution)\n# in one module was rejected — it violated INV_7 (708 lines vs 400 max), mixed threading\n# with async execution, and created an unmaintainable god class.\n\nimport asyncio\nfrom concurrent.futures import ThreadPoolExecutor\nfrom typing import Any\n\nfrom ..logger import belief_scope, logger\nfrom .event_bus import EventBus\nfrom .graph import TaskGraph\nfrom .lifecycle import JobLifecycle\nfrom .models import LogFilter, LogStats, Task, TaskStatus\nfrom .persistence import TaskLogPersistenceService, TaskPersistenceService\n\n\n# #region TaskManager [C:5] [TYPE Class] [SEMANTICS task, manager, lifecycle, execution, state]\n# @BRIEF Facade composing TaskGraph, EventBus, and JobLifecycle into a single interface.\n# @LAYER Core\n# @RELATION DEPENDS_ON -> [TaskPersistenceService]\n# @RELATION DEPENDS_ON -> [TaskLogPersistenceService]\n# @RELATION DEPENDS_ON -> [PluginLoader]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [JobLifecycle]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @PRE Plugin loader resolves plugin ids and persistence services are available.\n# @POST In-memory task graph, lifecycle scheduler, and log event bus stay consistent with\n# persisted task state.\n# @INVARIANT Task IDs are unique within the registry.\n# @INVARIANT Each task has exactly one status at any time.\n# @INVARIANT Log entries are never deleted after being added to a task.\n# @SIDE_EFFECT Spawns worker threads, flushes logs to database, and mutates task states.\n# @DATA_CONTRACT Input[plugin_id, params] -> Output[Task]\n# @RATIONALE Thin facade — all delegating methods are under 15 lines. Actual business logic\n# (registry CRUD, log buffering, lifecycle state machine) lives in extracted modules.\n# @REJECTED Keeping all five concerns in one class was rejected — it violated INV_7 (708-line\n# manager.py) and made the god class impossible to test or maintain independently.\nclass TaskManager:\n \"\"\"\n Facade composing TaskGraph (registry), EventBus (log/pub-sub), and\n JobLifecycle (state machine) into a single TaskManager interface.\n \"\"\"\n\n # #region __init__ [TYPE Function] [C:5]\n # @BRIEF Initialize sub-services, create add_log callback, start background flusher.\n # @PRE plugin_loader is initialized.\n # @POST TaskManager is ready to accept tasks.\n # @SIDE_EFFECT Starts background flusher thread and loads persisted task state into memory.\n def __init__(self, plugin_loader):\n with belief_scope(\"TaskManager.__init__\"):\n logger.reason(\"Initializing task manager runtime services\")\n self.plugin_loader = plugin_loader\n self.executor = ThreadPoolExecutor(max_workers=5)\n self.persistence_service = TaskPersistenceService()\n self.log_persistence_service = TaskLogPersistenceService()\n\n try:\n self.loop = asyncio.get_running_loop()\n except RuntimeError:\n self.loop = asyncio.get_event_loop()\n\n # Create sub-services\n self.graph = TaskGraph(self.persistence_service)\n self.event_bus = EventBus(self.log_persistence_service, self.loop)\n\n # Create add_log callback that lifecycle and plugins use\n self._add_log = self._make_add_log_callback()\n\n self.lifecycle = JobLifecycle(\n plugin_loader=self.plugin_loader,\n graph=self.graph,\n event_bus=self.event_bus,\n persistence_service=self.persistence_service,\n executor=self.executor,\n loop=self.loop,\n )\n\n # Load persisted tasks on startup\n self.graph.load_persisted_tasks()\n\n # Backward-compatible property aliases for tests\n self.tasks = self.graph.tasks\n self.task_futures = self.graph.task_futures\n self.subscribers = self.event_bus.subscribers\n self._log_buffer = self.event_bus._log_buffer\n self._log_buffer_lock = self.event_bus._log_buffer_lock\n self._flusher_stop_event = self.event_bus._flusher_stop_event\n self._flusher_thread = self.event_bus._flusher_thread\n\n logger.reflect(\n \"Task manager runtime initialized\",\n extra={\"task_count\": len(self.tasks)},\n )\n # #endregion __init__\n\n # #region _make_add_log_callback [C:2] [TYPE Function]\n # @BRIEF Create a closure for adding logs that looks up the task and delegates to EventBus.\n def _make_add_log_callback(self):\n def _add_log(task_id, level, message, source=\"system\", metadata=None, context=None):\n task = self.graph.get_task(task_id)\n if not task:\n return\n self.event_bus.add_log(\n task_id, level, message, source, metadata, context,\n task_logs_list=task.logs,\n )\n return _add_log\n # #endregion _make_add_log_callback\n\n # ── Deprecated legacy aliases (delegate to EventBus) ──\n\n # #region _flusher_loop [C:3] [TYPE Function] [SEMANTICS flush,background,thread]\n # @BRIEF Legacy alias delegating to EventBus._flusher_loop.\n def _flusher_loop(self):\n self.event_bus._flusher_loop()\n # #endregion _flusher_loop\n\n # #region _flush_logs [C:3] [TYPE Function] [SEMANTICS flush,batch,persistence]\n # @BRIEF Legacy alias delegating to EventBus._flush_logs.\n def _flush_logs(self):\n self.event_bus._flush_logs()\n # #endregion _flush_logs\n\n # #region _flush_task_logs [C:3] [TYPE Function] [SEMANTICS flush,single,persistence]\n # @BRIEF Legacy alias delegating to EventBus.flush_task_logs.\n def _flush_task_logs(self, task_id: str):\n self.event_bus.flush_task_logs(task_id)\n # #endregion _flush_task_logs\n\n # ── Task CRUD delegates to TaskGraph ──\n\n # #region get_task [TYPE Function] [C:2]\n # @BRIEF Retrieves a task by its ID.\n def get_task(self, task_id: str) -> Task | None:\n return self.graph.get_task(task_id)\n # #endregion get_task\n\n # #region get_all_tasks [TYPE Function] [C:1]\n # @BRIEF Retrieves all registered tasks.\n def get_all_tasks(self) -> list[Task]:\n return self.graph.get_all_tasks()\n # #endregion get_all_tasks\n\n # #region get_tasks [TYPE Function] [C:3]\n # @BRIEF Retrieves tasks with pagination and optional status filter.\n def get_tasks(\n self,\n limit: int = 10,\n offset: int = 0,\n status: TaskStatus | None = None,\n plugin_ids: list[str] | None = None,\n completed_only: bool = False,\n ) -> list[Task]:\n return self.graph.get_tasks(limit, offset, status, plugin_ids, completed_only)\n # #endregion get_tasks\n\n # #region load_persisted_tasks [TYPE Function] [C:2]\n # @BRIEF Load persisted tasks using persistence service.\n def load_persisted_tasks(self) -> None:\n self.graph.load_persisted_tasks(limit=100)\n # #endregion load_persisted_tasks\n\n # #region clear_tasks [TYPE Function] [C:4]\n # @BRIEF Clears tasks based on status filter (also deletes associated logs).\n # @SIDE_EFFECT Removes tasks from registry and persistence; cancels futures.\n def clear_tasks(self, status: TaskStatus | None = None) -> int:\n with belief_scope(\"TaskManager.clear_tasks\"):\n tasks_to_remove = []\n for task_id, task in list(self.tasks.items()):\n should_remove = False\n if status:\n if task.status == status:\n should_remove = True\n else:\n if task.status not in [\n TaskStatus.RUNNING,\n TaskStatus.AWAITING_INPUT,\n TaskStatus.AWAITING_MAPPING,\n ]:\n should_remove = True\n if should_remove:\n tasks_to_remove.append(task_id)\n\n # Delete logs first, then remove tasks\n if tasks_to_remove:\n self.event_bus.delete_logs_for_tasks(tasks_to_remove)\n removed = self.graph.remove_tasks(tasks_to_remove)\n logger.info(f\"Cleared {removed} tasks.\")\n return removed\n # #endregion clear_tasks\n\n # ── Log delegates to EventBus ──\n\n # #region get_task_logs [TYPE Function] [C:3]\n # @BRIEF Retrieves logs for a specific task (from memory or persistence).\n def get_task_logs(\n self, task_id: str, log_filter: LogFilter | None = None\n ) -> list:\n task = self.graph.get_task(task_id)\n task_status = task.status if task else None\n task_logs = task.logs if task else []\n return self.event_bus.get_task_logs(\n task_id, log_filter, task_status=task_status, task_logs=task_logs\n )\n # #endregion get_task_logs\n\n # #region get_task_log_stats [TYPE Function] [C:2]\n # @BRIEF Get statistics about logs for a task.\n def get_task_log_stats(self, task_id: str) -> LogStats:\n return self.event_bus.get_task_log_stats(task_id)\n # #endregion get_task_log_stats\n\n # #region get_task_log_sources [TYPE Function] [C:2]\n # @BRIEF Get unique sources for a task's logs.\n def get_task_log_sources(self, task_id: str) -> list[str]:\n return self.event_bus.get_task_log_sources(task_id)\n # #endregion get_task_log_sources\n\n # ── Subscription delegates to EventBus ──\n\n # #region subscribe_logs [TYPE Function] [C:2]\n # @BRIEF Subscribes to real-time logs for a task.\n async def subscribe_logs(self, task_id: str) -> asyncio.Queue:\n return await self.event_bus.subscribe_logs(task_id)\n # #endregion subscribe_logs\n\n # #region unsubscribe_logs [TYPE Function] [C:2]\n # @BRIEF Unsubscribes from real-time logs for a task.\n def unsubscribe_logs(self, task_id: str, queue: asyncio.Queue):\n self.event_bus.unsubscribe_logs(task_id, queue)\n # #endregion unsubscribe_logs\n\n # ── Lifecycle delegates to JobLifecycle ──\n\n # #region create_task [TYPE Function] [C:4]\n # @BRIEF Creates and queues a new task for execution.\n async def create_task(\n self, plugin_id: str, params: dict[str, Any], user_id: str | None = None\n ) -> Task:\n return await self.lifecycle.create_task(\n plugin_id, params, user_id,\n add_log_callback=self._add_log,\n )\n # #endregion create_task\n\n # #region _run_task [TYPE Function] [C:4]\n # @BRIEF Internal method to execute a task with TaskContext support (delegates to lifecycle).\n async def _run_task(self, task_id: str):\n await self.lifecycle._run_task(task_id, add_log_callback=self._add_log)\n # #endregion _run_task\n\n # #region resolve_task [TYPE Function] [C:3]\n # @BRIEF Resumes a task that is awaiting mapping.\n async def resolve_task(self, task_id: str, resolution_params: dict[str, Any]):\n await self.lifecycle.resolve_task(task_id, resolution_params)\n # #endregion resolve_task\n\n # #region wait_for_resolution [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for a resolution signal.\n async def wait_for_resolution(self, task_id: str):\n await self.lifecycle.wait_for_resolution(task_id)\n # #endregion wait_for_resolution\n\n # #region wait_for_input [TYPE Function] [C:3]\n # @BRIEF Pauses execution and waits for user input.\n async def wait_for_input(self, task_id: str):\n await self.lifecycle.wait_for_input(task_id)\n # #endregion wait_for_input\n\n # #region await_input [TYPE Function] [C:3]\n # @BRIEF Transition a task to AWAITING_INPUT state with input request.\n def await_input(self, task_id: str, input_request: dict[str, Any]) -> None:\n self.lifecycle.await_input(\n task_id, input_request,\n add_log_callback=self._add_log,\n )\n # #endregion await_input\n\n # #region resume_task_with_password [TYPE Function] [C:3]\n # @BRIEF Resume a task that is awaiting input with provided passwords.\n def resume_task_with_password(\n self, task_id: str, passwords: dict[str, str]\n ) -> None:\n self.lifecycle.resume_task_with_password(\n task_id, passwords,\n add_log_callback=self._add_log,\n )\n # #endregion resume_task_with_password\n\n # ── Dataset event delegates to JobLifecycle ──\n\n # #region subscribe_dataset_events [TYPE Function] [C:2]\n # @BRIEF Subscribe to dataset.updated events for an environment.\n async def subscribe_dataset_events(self, env_id: str) -> asyncio.Queue:\n return await self.lifecycle.subscribe_dataset_events(env_id)\n # #endregion subscribe_dataset_events\n\n # #region unsubscribe_dataset_events [TYPE Function] [C:2]\n # @BRIEF Unsubscribe from dataset.updated events.\n def unsubscribe_dataset_events(self, env_id: str, queue: asyncio.Queue):\n self.lifecycle.unsubscribe_dataset_events(env_id, queue)\n # #endregion unsubscribe_dataset_events\n# #endregion TaskManager\n# #endregion TaskManagerModule\n" }, { "contract_id": "TaskManager", "contract_type": "Class", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 48, - "end_line": 335, + "start_line": 47, + "end_line": 334, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -26771,8 +27644,8 @@ "contract_id": "_make_add_log_callback", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 128, - "end_line": 140, + "start_line": 127, + "end_line": 139, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -26789,8 +27662,8 @@ "contract_id": "_flusher_loop", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 144, - "end_line": 148, + "start_line": 143, + "end_line": 147, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -26807,8 +27680,8 @@ "contract_id": "_flush_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 150, - "end_line": 154, + "start_line": 149, + "end_line": 153, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -26825,8 +27698,8 @@ "contract_id": "_flush_task_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 156, - "end_line": 160, + "start_line": 155, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -26843,8 +27716,8 @@ "contract_id": "get_task", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 164, - "end_line": 168, + "start_line": 163, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -26861,8 +27734,8 @@ "contract_id": "get_all_tasks", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 170, - "end_line": 174, + "start_line": 169, + "end_line": 173, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -26879,8 +27752,8 @@ "contract_id": "get_tasks", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 176, - "end_line": 187, + "start_line": 175, + "end_line": 186, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -26897,8 +27770,8 @@ "contract_id": "load_persisted_tasks", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 189, - "end_line": 193, + "start_line": 188, + "end_line": 192, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -26915,8 +27788,8 @@ "contract_id": "clear_tasks", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 195, - "end_line": 222, + "start_line": 194, + "end_line": 221, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -26934,8 +27807,8 @@ "contract_id": "get_task_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 226, - "end_line": 237, + "start_line": 225, + "end_line": 236, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -26952,8 +27825,8 @@ "contract_id": "get_task_log_stats", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 239, - "end_line": 243, + "start_line": 238, + "end_line": 242, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -26970,8 +27843,8 @@ "contract_id": "get_task_log_sources", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 245, - "end_line": 249, + "start_line": 244, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -26988,8 +27861,8 @@ "contract_id": "subscribe_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 253, - "end_line": 257, + "start_line": 252, + "end_line": 256, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27006,8 +27879,8 @@ "contract_id": "unsubscribe_logs", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 259, - "end_line": 263, + "start_line": 258, + "end_line": 262, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27024,8 +27897,8 @@ "contract_id": "create_task", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 267, - "end_line": 276, + "start_line": 266, + "end_line": 275, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -27042,8 +27915,8 @@ "contract_id": "_run_task", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 278, - "end_line": 282, + "start_line": 277, + "end_line": 281, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -27060,8 +27933,8 @@ "contract_id": "resolve_task", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 284, - "end_line": 288, + "start_line": 283, + "end_line": 287, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27078,8 +27951,8 @@ "contract_id": "wait_for_resolution", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 290, - "end_line": 294, + "start_line": 289, + "end_line": 293, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27096,8 +27969,8 @@ "contract_id": "wait_for_input", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 296, - "end_line": 300, + "start_line": 295, + "end_line": 299, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27114,8 +27987,8 @@ "contract_id": "await_input", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 302, - "end_line": 309, + "start_line": 301, + "end_line": 308, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27132,8 +28005,8 @@ "contract_id": "resume_task_with_password", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 311, - "end_line": 320, + "start_line": 310, + "end_line": 319, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27150,8 +28023,8 @@ "contract_id": "subscribe_dataset_events", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 324, - "end_line": 328, + "start_line": 323, + "end_line": 327, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27168,8 +28041,8 @@ "contract_id": "unsubscribe_dataset_events", "contract_type": "Function", "file_path": "backend/src/core/task_manager/manager.py", - "start_line": 330, - "end_line": 334, + "start_line": 329, + "end_line": 333, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -27187,7 +28060,7 @@ "contract_type": "Module", "file_path": "backend/src/core/task_manager/models.py", "start_line": 1, - "end_line": 116, + "end_line": 115, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -27217,7 +28090,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskManagerModels [C:5] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status]\n# @BRIEF Defines the data models and enumerations used by the Task Manager.\n# @LAYER Domain\n# @RELATION CALLED_BY -> [TaskManager]\n# @RELATION CALLED_BY -> [TaskManagerPackage]\n# @INVARIANT Task IDs are immutable once created.\n# @CONSTRAINT: Must use Pydantic for data validation.\n# @PRE Task manager initialized\n# @POST Task models exported with immutable IDs\n# @SIDE_EFFECT Defines task data schema\n# @DATA_CONTRACT TaskInput -> TaskModel\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\nimport uuid\n\nfrom pydantic import BaseModel, Field\n\n\n# #region TaskStatus [TYPE Enum]\nclass TaskStatus(str, Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCESS = \"SUCCESS\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n AWAITING_INPUT = \"AWAITING_INPUT\"\n# #endregion TaskStatus\n# #region LogLevel [TYPE Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n# #endregion LogLevel\n# #region LogEntry [C:2] [TYPE Class]\n# @BRIEF A Pydantic model representing a single, structured log entry associated with a task.\n#\n# {\n# required_fields: {message: str},\n# optional_fields: {timestamp: datetime, level: str, source: str, context: dict, metadata: dict}\n# }\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n level: str = Field(default=\"INFO\")\n message: str\n source: str = Field(\n default=\"system\"\n ) # Component attribution: plugin, superset_api, git, etc.\n context: dict[str, Any] | None = (\n None # Legacy field, kept for backward compatibility\n )\n metadata: dict[str, Any] | None = (\n None # Structured metadata (e.g., dashboard_id, progress)\n )\n# #endregion LogEntry\n# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n class Config:\n from_attributes = True\n# #endregion TaskLog\n# #region LogFilter [TYPE Class]\nclass LogFilter(BaseModel):\n level: str | None = None # Filter by log level\n source: str | None = None # Filter by source component\n search: str | None = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n# #endregion LogFilter\n# #region LogStats [C:1] [TYPE Class] [SEMANTICS log, stats, aggregation, pydantic]\n# @BRIEF Statistics about log entries for a task.\n# @RELATION DEPENDS_ON -> [TaskLog]\nclass LogStats(BaseModel):\n total_count: int\n by_level: dict[str, int] # {\"INFO\": 10, \"ERROR\": 2}\n by_source: dict[str, int] # {\"plugin\": 5, \"superset_api\": 7}\n# #endregion LogStats\n# #region Task [C:3] [TYPE Class] [SEMANTICS task, job, execution, state, pydantic]\n# @BRIEF A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.\n# @RELATION DEPENDS_ON -> [TaskStatus]\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass Task(BaseModel):\n id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n plugin_id: str\n status: TaskStatus = TaskStatus.PENDING\n started_at: datetime | None = None\n finished_at: datetime | None = None\n user_id: str | None = None\n logs: list[LogEntry] = Field(default_factory=list)\n params: dict[str, Any] = Field(default_factory=dict)\n input_required: bool = False\n input_request: dict[str, Any] | None = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Any | None = None\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status.\n # @PRE If status is AWAITING_INPUT, input_request must be provided.\n # @POST Task instance is created or ValueError is raised.\n # @PARAM **data - Keyword arguments for model initialization.\n def __init__(self, **data):\n super().__init__(**data)\n if self.status == TaskStatus.AWAITING_INPUT and not self.input_request:\n raise ValueError(\"input_request is required when status is AWAITING_INPUT\")\n # #endregion __init__\n# #endregion Task\n# #endregion TaskManagerModels\n" + "body": "# #region TaskManagerModels [C:5] [TYPE Module] [SEMANTICS pydantic, task, model, validate, task-status]\n# @BRIEF Defines the data models and enumerations used by the Task Manager.\n# @LAYER Domain\n# @RELATION CALLED_BY -> [TaskManager]\n# @RELATION CALLED_BY -> [TaskManagerPackage]\n# @INVARIANT Task IDs are immutable once created.\n# @CONSTRAINT: Must use Pydantic for data validation.\n# @PRE Task manager initialized\n# @POST Task models exported with immutable IDs\n# @SIDE_EFFECT Defines task data schema\n# @DATA_CONTRACT TaskInput -> TaskModel\nfrom datetime import UTC, datetime\nfrom enum import Enum\nfrom typing import Any\nimport uuid\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region TaskStatus [TYPE Enum]\nclass TaskStatus(str, Enum):\n PENDING = \"PENDING\"\n RUNNING = \"RUNNING\"\n SUCCESS = \"SUCCESS\"\n FAILED = \"FAILED\"\n AWAITING_MAPPING = \"AWAITING_MAPPING\"\n AWAITING_INPUT = \"AWAITING_INPUT\"\n# #endregion TaskStatus\n# #region LogLevel [TYPE Enum]\nclass LogLevel(str, Enum):\n DEBUG = \"DEBUG\"\n INFO = \"INFO\"\n WARNING = \"WARNING\"\n ERROR = \"ERROR\"\n# #endregion LogLevel\n# #region LogEntry [C:2] [TYPE Class]\n# @BRIEF A Pydantic model representing a single, structured log entry associated with a task.\n#\n# {\n# required_fields: {message: str},\n# optional_fields: {timestamp: datetime, level: str, source: str, context: dict, metadata: dict}\n# }\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n level: str = Field(default=\"INFO\")\n message: str\n source: str = Field(\n default=\"system\"\n ) # Component attribution: plugin, superset_api, git, etc.\n context: dict[str, Any] | None = (\n None # Legacy field, kept for backward compatibility\n )\n metadata: dict[str, Any] | None = (\n None # Structured metadata (e.g., dashboard_id, progress)\n )\n# #endregion LogEntry\n# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n model_config = ConfigDict(from_attributes=True)\n# #endregion TaskLog\n# #region LogFilter [TYPE Class]\nclass LogFilter(BaseModel):\n level: str | None = None # Filter by log level\n source: str | None = None # Filter by source component\n search: str | None = None # Text search in message\n offset: int = Field(default=0, ge=0)\n limit: int = Field(default=100, ge=1, le=1000)\n# #endregion LogFilter\n# #region LogStats [C:1] [TYPE Class] [SEMANTICS log, stats, aggregation, pydantic]\n# @BRIEF Statistics about log entries for a task.\n# @RELATION DEPENDS_ON -> [TaskLog]\nclass LogStats(BaseModel):\n total_count: int\n by_level: dict[str, int] # {\"INFO\": 10, \"ERROR\": 2}\n by_source: dict[str, int] # {\"plugin\": 5, \"superset_api\": 7}\n# #endregion LogStats\n# #region Task [C:3] [TYPE Class] [SEMANTICS task, job, execution, state, pydantic]\n# @BRIEF A Pydantic model representing a single execution instance of a plugin, including its status, parameters, and logs.\n# @RELATION DEPENDS_ON -> [TaskStatus]\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [TaskManager]\nclass Task(BaseModel):\n id: str = Field(default_factory=lambda: str(uuid.uuid4()))\n plugin_id: str\n status: TaskStatus = TaskStatus.PENDING\n started_at: datetime | None = None\n finished_at: datetime | None = None\n user_id: str | None = None\n logs: list[LogEntry] = Field(default_factory=list)\n params: dict[str, Any] = Field(default_factory=dict)\n input_required: bool = False\n input_request: dict[str, Any] | None = None\n # Result payload can be dict/list/scalar depending on plugin and legacy records.\n result: Any | None = None\n # #region __init__ [TYPE Function]\n # @PURPOSE: Initializes the Task model and validates input_request for AWAITING_INPUT status.\n # @PRE If status is AWAITING_INPUT, input_request must be provided.\n # @POST Task instance is created or ValueError is raised.\n # @PARAM **data - Keyword arguments for model initialization.\n def __init__(self, **data):\n super().__init__(**data)\n if self.status == TaskStatus.AWAITING_INPUT and not self.input_request:\n raise ValueError(\"input_request is required when status is AWAITING_INPUT\")\n # #endregion __init__\n# #endregion Task\n# #endregion TaskManagerModels\n" }, { "contract_id": "TaskStatus", @@ -27254,7 +28127,7 @@ "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", "start_line": 57, - "end_line": 70, + "end_line": 69, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27272,14 +28145,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n class Config:\n from_attributes = True\n# #endregion TaskLog\n" + "body": "# #region TaskLog [C:3] [TYPE Class] [SEMANTICS task, log, persistent, pydantic]\n# @BRIEF A Pydantic model representing a persisted log entry from the database.\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\nclass TaskLog(BaseModel):\n id: int\n task_id: str\n timestamp: datetime\n level: str\n source: str\n message: str\n metadata: dict[str, Any] | None = None\n model_config = ConfigDict(from_attributes=True)\n# #endregion TaskLog\n" }, { "contract_id": "LogFilter", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 71, - "end_line": 78, + "start_line": 70, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -27293,8 +28166,8 @@ "contract_id": "LogStats", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 79, - "end_line": 86, + "start_line": 78, + "end_line": 85, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -27318,8 +28191,8 @@ "contract_id": "Task", "contract_type": "Class", "file_path": "backend/src/core/task_manager/models.py", - "start_line": 87, - "end_line": 115, + "start_line": 86, + "end_line": 114, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -27392,7 +28265,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskPersistenceModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, task, search, task-persistence-service]\n# @BRIEF Handles the persistence of tasks using SQLAlchemy and the tasks.db database.\n# @LAYER Core\n# @PRE Tasks database must be initialized with TaskRecord and TaskLogRecord schemas.\n# @POST Provides reliable storage and retrieval for task metadata and logs.\n# @SIDE_EFFECT Performs database I/O on tasks.db.\n# @DATA_CONTRACT Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @INVARIANT Database schema must match the TaskRecord model structure.\nfrom datetime import datetime\nimport json\nimport re\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.mapping import Environment\nfrom ...models.task import TaskLogRecord, TaskRecord\nfrom ..database import TasksSessionLocal\nfrom ..logger import belief_scope, logger\nfrom .models import LogEntry, LogFilter, LogStats, Task, TaskLog, TaskStatus\n\n\n# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @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.\n# @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.\n# @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.\n# @DATA_CONTRACT Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE persist_invalid_task_type -> raises Exception\n# @TEST_EDGE load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE value is an arbitrary database value\n # @POST Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE value is an ISO string or datetime object\n # @POST Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE Session is active\n # @POST Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION DEPENDS_ON -> [Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE None.\n # @POST Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE isinstance(tasks, list)\n # @POST All tasks in list are persisted.\n # @PARAM tasks (List[Task]) - The list of tasks to persist.\n # @RELATION CALLS -> [persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE task_ids is a list of strings.\n # @POST Specified task records deleted from database.\n # @PARAM task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT Deletes rows from task_records table.\n # @RELATION DEPENDS_ON -> [TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n# #region TaskLogPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, log, sqlalchemy]\n# @BRIEF Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @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.\n# @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.\n# @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.\n# @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]]\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE empty_log_list -> no-op behavior\n# @TEST_EDGE add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE config is provided or defaults are used\n # @POST Service is ready for log persistence\n def __init__(self, config=None):\n pass\n # #endregion __init__\n # #region add_logs [TYPE Function] [C:3]\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE logs is a list of LogEntry objects.\n # @POST All logs inserted into task_logs table.\n # @PARAM task_id (str) - The task ID.\n # @PARAM logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT Writes to task_logs table.\n # @DATA_CONTRACT Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def add_logs(self, task_id: str, logs: list[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n for log in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log.timestamp,\n level=log.level,\n source=log.source or \"system\",\n message=log.message,\n metadata_json=json.dumps(log.metadata)\n if log.metadata\n else None,\n )\n session.add(record)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion add_logs\n # #region get_logs [TYPE Function] [C:3]\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE task_id is a valid task ID.\n # @POST Returns list of TaskLog objects matching filters.\n # @PARAM task_id (str) - The task ID.\n # @PARAM log_filter (LogFilter) - Filter parameters.\n # @RETURN List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n # @RELATION DEPENDS_ON -> [LogFilter]\n # @RELATION DEPENDS_ON -> [TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> list[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n return logs\n finally:\n session.close()\n # #endregion get_logs\n # #region get_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE task_id is a valid task ID.\n # @POST Returns LogStats with counts by level and source.\n # @PARAM task_id (str) - The task ID.\n # @RETURN LogStats - Statistics about task logs.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n # @RELATION DEPENDS_ON -> [LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n # Get counts by level\n from sqlalchemy import func\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n by_level = {level: count for level, count in level_counts}\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n by_source = {source: count for source, count in source_counts}\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n # #endregion get_log_stats\n # #region get_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE task_id is a valid task ID.\n # @POST Returns list of unique source strings.\n # @PARAM task_id (str) - The task ID.\n # @RETURN List[str] - Unique source names.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def get_sources(self, task_id: str) -> list[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n return [s[0] for s in sources]\n finally:\n session.close()\n # #endregion get_sources\n # #region delete_logs_for_task [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE task_id is a valid task ID.\n # @POST All logs for the task are deleted.\n # @PARAM task_id (str) - The task ID.\n # @SIDE_EFFECT Deletes from task_logs table.\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_task\n # #region delete_logs_for_tasks [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE task_ids is a list of task IDs.\n # @POST All logs for the tasks are deleted.\n # @PARAM task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT Deletes rows from task_logs table.\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_tasks\n# #endregion TaskLogPersistenceService\n# #endregion TaskPersistenceModule\n" + "body": "# #region TaskPersistenceModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, task, search, task-persistence-service]\n# @BRIEF Handles the persistence of tasks using SQLAlchemy and the tasks.db database.\n# @LAYER Core\n# @PRE Tasks database must be initialized with TaskRecord and TaskLogRecord schemas.\n# @POST Provides reliable storage and retrieval for task metadata and logs.\n# @SIDE_EFFECT Performs database I/O on tasks.db.\n# @DATA_CONTRACT Input[Task, LogEntry] -> Model[TaskRecord, TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @INVARIANT Database schema must match the TaskRecord model structure.\nfrom datetime import UTC, datetime\nimport json\nimport re\n\nfrom sqlalchemy.orm import Session\n\nfrom ...models.mapping import Environment\nfrom ...models.task import TaskLogRecord, TaskRecord\nfrom ..database import TasksSessionLocal\nfrom ..logger import belief_scope, logger\nfrom .models import LogEntry, LogFilter, LogStats, Task, TaskLog, TaskStatus\n\n\n# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @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.\n# @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.\n# @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.\n# @DATA_CONTRACT Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE persist_invalid_task_type -> raises Exception\n# @TEST_EDGE load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE value is an arbitrary database value\n # @POST Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE value is an ISO string or datetime object\n # @POST Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE Session is active\n # @POST Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION DEPENDS_ON -> [Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE None.\n # @POST Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.model_dump()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE isinstance(tasks, list)\n # @POST All tasks in list are persisted.\n # @PARAM tasks (List[Task]) - The list of tasks to persist.\n # @RELATION CALLS -> [persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.now(UTC)\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE task_ids is a list of strings.\n # @POST Specified task records deleted from database.\n # @PARAM task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT Deletes rows from task_records table.\n # @RELATION DEPENDS_ON -> [TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n# #region TaskLogPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, log, sqlalchemy]\n# @BRIEF Provides methods to store, query, summarize, and delete task log rows in the task_logs table.\n# @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.\n# @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.\n# @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.\n# @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]]\n# @RELATION DEPENDS_ON -> [TaskLogRecord]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [EventBus]\n# @INVARIANT Log entries are batch-inserted for performance.\n#\n# @TEST_CONTRACT TaskLogPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"add_logs efficiently saves logs to the database\",\n# \"get_logs retrieves properly filtered LogEntry objects\"\n# ]\n# }\n# @TEST_FIXTURE valid_log_batch -> {\"task_id\": \"123\", \"logs\": [{\"level\": \"INFO\", \"message\": \"msg\"}]}\n# @TEST_EDGE empty_log_list -> no-op behavior\n# @TEST_EDGE add_logs_db_error -> rollback and log error\n# @TEST_INVARIANT accurate_log_aggregation -> verifies: [valid_log_batch]\nclass TaskLogPersistenceService:\n \"\"\"\n Service for persisting and querying task logs.\n Supports batch inserts, filtering, and statistics.\n \"\"\"\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the TaskLogPersistenceService\n # @PRE config is provided or defaults are used\n # @POST Service is ready for log persistence\n def __init__(self, config=None):\n pass\n # #endregion __init__\n # #region add_logs [TYPE Function] [C:3]\n # @PURPOSE: Batch insert log entries for a task.\n # @PRE logs is a list of LogEntry objects.\n # @POST All logs inserted into task_logs table.\n # @PARAM task_id (str) - The task ID.\n # @PARAM logs (List[LogEntry]) - Log entries to insert.\n # @SIDE_EFFECT Writes to task_logs table.\n # @DATA_CONTRACT Input[List[LogEntry]] -> Model[TaskLogRecord]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def add_logs(self, task_id: str, logs: list[LogEntry]) -> None:\n if not logs:\n return\n with belief_scope(\"TaskLogPersistenceService.add_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n for log in logs:\n record = TaskLogRecord(\n task_id=task_id,\n timestamp=log.timestamp,\n level=log.level,\n source=log.source or \"system\",\n message=log.message,\n metadata_json=json.dumps(log.metadata)\n if log.metadata\n else None,\n )\n session.add(record)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to add logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion add_logs\n # #region get_logs [TYPE Function] [C:3]\n # @PURPOSE: Query logs for a task with filtering and pagination.\n # @PRE task_id is a valid task ID.\n # @POST Returns list of TaskLog objects matching filters.\n # @PARAM task_id (str) - The task ID.\n # @PARAM log_filter (LogFilter) - Filter parameters.\n # @RETURN List[TaskLog] - Filtered log entries.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[List[TaskLog]]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n # @RELATION DEPENDS_ON -> [LogFilter]\n # @RELATION DEPENDS_ON -> [TaskLog]\n def get_logs(self, task_id: str, log_filter: LogFilter) -> list[TaskLog]:\n with belief_scope(\"TaskLogPersistenceService.get_logs\", f\"task_id={task_id}\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n )\n # Apply filters\n if log_filter.level:\n query = query.filter(\n TaskLogRecord.level == log_filter.level.upper()\n )\n if log_filter.source:\n query = query.filter(TaskLogRecord.source == log_filter.source)\n if log_filter.search:\n search_pattern = f\"%{log_filter.search}%\"\n query = query.filter(TaskLogRecord.message.ilike(search_pattern))\n # Order by timestamp ascending (oldest first)\n query = query.order_by(TaskLogRecord.timestamp.asc())\n # Apply pagination\n records = query.offset(log_filter.offset).limit(log_filter.limit).all()\n logs = []\n for record in records:\n metadata = None\n if record.metadata_json:\n try:\n metadata = json.loads(record.metadata_json)\n except json.JSONDecodeError:\n metadata = None\n logs.append(\n TaskLog(\n id=record.id,\n task_id=record.task_id,\n timestamp=record.timestamp,\n level=record.level,\n source=record.source,\n message=record.message,\n metadata=metadata,\n )\n )\n return logs\n finally:\n session.close()\n # #endregion get_logs\n # #region get_log_stats [TYPE Function] [C:3]\n # @PURPOSE: Get statistics about logs for a task.\n # @PRE task_id is a valid task ID.\n # @POST Returns LogStats with counts by level and source.\n # @PARAM task_id (str) - The task ID.\n # @RETURN LogStats - Statistics about task logs.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[LogStats]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n # @RELATION DEPENDS_ON -> [LogStats]\n def get_log_stats(self, task_id: str) -> LogStats:\n with belief_scope(\n \"TaskLogPersistenceService.get_log_stats\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n # Get total count\n total_count = (\n session.query(TaskLogRecord)\n .filter(TaskLogRecord.task_id == task_id)\n .count()\n )\n # Get counts by level\n from sqlalchemy import func\n level_counts = (\n session.query(TaskLogRecord.level, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.level)\n .all()\n )\n by_level = {level: count for level, count in level_counts}\n # Get counts by source\n source_counts = (\n session.query(TaskLogRecord.source, func.count(TaskLogRecord.id))\n .filter(TaskLogRecord.task_id == task_id)\n .group_by(TaskLogRecord.source)\n .all()\n )\n by_source = {source: count for source, count in source_counts}\n return LogStats(\n total_count=total_count, by_level=by_level, by_source=by_source\n )\n finally:\n session.close()\n # #endregion get_log_stats\n # #region get_sources [TYPE Function] [C:3]\n # @PURPOSE: Get unique sources for a task's logs.\n # @PRE task_id is a valid task ID.\n # @POST Returns list of unique source strings.\n # @PARAM task_id (str) - The task ID.\n # @RETURN List[str] - Unique source names.\n # @DATA_CONTRACT Model[TaskLogRecord] -> Output[List[str]]\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def get_sources(self, task_id: str) -> list[str]:\n with belief_scope(\n \"TaskLogPersistenceService.get_sources\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n from sqlalchemy import distinct\n sources = (\n session.query(distinct(TaskLogRecord.source))\n .filter(TaskLogRecord.task_id == task_id)\n .all()\n )\n return [s[0] for s in sources]\n finally:\n session.close()\n # #endregion get_sources\n # #region delete_logs_for_task [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for a specific task.\n # @PRE task_id is a valid task ID.\n # @POST All logs for the task are deleted.\n # @PARAM task_id (str) - The task ID.\n # @SIDE_EFFECT Deletes from task_logs table.\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def delete_logs_for_task(self, task_id: str) -> None:\n with belief_scope(\n \"TaskLogPersistenceService.delete_logs_for_task\", f\"task_id={task_id}\"\n ):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id == task_id\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for task {task_id}: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_task\n # #region delete_logs_for_tasks [TYPE Function] [C:3]\n # @PURPOSE: Delete all logs for multiple tasks.\n # @PRE task_ids is a list of task IDs.\n # @POST All logs for the tasks are deleted.\n # @PARAM task_ids (List[str]) - List of task IDs.\n # @SIDE_EFFECT Deletes rows from task_logs table.\n # @RELATION DEPENDS_ON -> [TaskLogRecord]\n def delete_logs_for_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskLogPersistenceService.delete_logs_for_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskLogRecord).filter(\n TaskLogRecord.task_id.in_(task_ids)\n ).delete(synchronize_session=False)\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete logs for tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_logs_for_tasks\n# #endregion TaskLogPersistenceService\n# #endregion TaskPersistenceModule\n" }, { "contract_id": "TaskPersistenceService", @@ -27447,7 +28320,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @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.\n# @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.\n# @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.\n# @DATA_CONTRACT Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE persist_invalid_task_type -> raises Exception\n# @TEST_EDGE load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE value is an arbitrary database value\n # @POST Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE value is an ISO string or datetime object\n # @POST Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE Session is active\n # @POST Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION DEPENDS_ON -> [Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE None.\n # @POST Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE isinstance(tasks, list)\n # @POST All tasks in list are persisted.\n # @PARAM tasks (List[Task]) - The list of tasks to persist.\n # @RELATION CALLS -> [persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE task_ids is a list of strings.\n # @POST Specified task records deleted from database.\n # @PARAM task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT Deletes rows from task_records table.\n # @RELATION DEPENDS_ON -> [TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n" + "body": "# #region TaskPersistenceService [C:5] [TYPE Class] [SEMANTICS persistence, service, database, sqlalchemy]\n# @BRIEF Provides methods to save, load, and delete task records in tasks.db using SQLAlchemy models.\n# @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.\n# @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.\n# @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.\n# @DATA_CONTRACT Input[Task | List[Task] | List[str] | Query(limit:int,status:Optional[TaskStatus])] -> Model[TaskRecord, Environment] -> Output[None | List[Task]]\n# @RELATION DEPENDS_ON -> [TasksSessionLocal]\n# @RELATION DEPENDS_ON -> [TaskRecord]\n# @RELATION DEPENDS_ON -> [Environment]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [TaskGraph]\n# @INVARIANT Persistence must handle potentially missing task fields natively.\n#\n# @TEST_CONTRACT TaskPersistenceContract ->\n# {\n# required_fields: {},\n# invariants: [\n# \"persist_task creates or updates a record\",\n# \"load_tasks retrieves valid Task instances\",\n# \"delete_tasks correctly removes records from the database\"\n# ]\n# }\n# @TEST_FIXTURE valid_task_persistence -> {\"task_id\": \"123\", \"status\": \"PENDING\"}\n# @TEST_EDGE persist_invalid_task_type -> raises Exception\n# @TEST_EDGE load_corrupt_json_params -> handled gracefully\n# @TEST_INVARIANT accurate_round_trip -> verifies: [valid_task_persistence, load_corrupt_json_params]\nclass TaskPersistenceService:\n # #region _json_load_if_needed [TYPE Function] [C:1]\n # @PURPOSE: Safely load JSON strings from DB if necessary\n # @PRE value is an arbitrary database value\n # @POST Returns parsed JSON object, list, string, or primitive\n @staticmethod\n def _json_load_if_needed(value):\n # Hot-path utility — no entry/exit logging to reduce noise\n if value is None:\n return None\n if isinstance(value, (dict, list)):\n return value\n if isinstance(value, str):\n stripped = value.strip()\n if stripped == \"\" or stripped.lower() == \"null\":\n return None\n try:\n return json.loads(stripped)\n except json.JSONDecodeError:\n return value\n return value\n # #endregion _json_load_if_needed\n # #region _parse_datetime [TYPE Function] [C:1]\n # @PURPOSE: Safely parse a datetime string from the database\n # @PRE value is an ISO string or datetime object\n # @POST Returns datetime object or None\n @staticmethod\n def _parse_datetime(value):\n # Hot-path utility — no entry/exit logging to reduce startup noise\n if value is None or isinstance(value, datetime):\n return value\n if isinstance(value, str):\n try:\n return datetime.fromisoformat(value)\n except ValueError:\n return None\n return None\n # #endregion _parse_datetime\n # #region _resolve_environment_id [TYPE Function] [C:3]\n # @PURPOSE: Resolve environment id into existing environments.id value to satisfy FK constraints.\n # @PRE Session is active\n # @POST Returns existing environments.id or None when unresolved.\n # @DATA_CONTRACT Input[env_id: Optional[str]] -> Output[Optional[str]]\n # @RELATION DEPENDS_ON -> [Environment]\n @staticmethod\n def _resolve_environment_id(\n session: Session, env_id: str | None\n ) -> str | None:\n with belief_scope(\"_resolve_environment_id\"):\n raw_value = str(env_id or \"\").strip()\n if not raw_value:\n return None\n # 1) Direct match by primary key.\n by_id = (\n session.query(Environment).filter(Environment.id == raw_value).first()\n )\n if by_id:\n return str(by_id.id)\n # 2) Exact match by name.\n by_name = (\n session.query(Environment).filter(Environment.name == raw_value).first()\n )\n if by_name:\n return str(by_name.id)\n # 3) Slug-like match (e.g. \"ss-dev\" -> \"SS DEV\").\n def normalize_token(value: str) -> str:\n lowered = str(value or \"\").strip().lower()\n return re.sub(r\"[^a-z0-9]+\", \"-\", lowered).strip(\"-\")\n target_token = normalize_token(raw_value)\n if not target_token:\n return None\n for env in session.query(Environment).all():\n if (\n normalize_token(env.id) == target_token\n or normalize_token(env.name) == target_token\n ):\n return str(env.id)\n return None\n # #endregion _resolve_environment_id\n # #region __init__ [TYPE Function] [C:3]\n # @PURPOSE: Initializes the persistence service.\n # @PRE None.\n # @POST Service is ready.\n def __init__(self):\n with belief_scope(\"TaskPersistenceService.__init__\"):\n # We use TasksSessionLocal from database.py\n pass\n # #endregion __init__\n # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.model_dump()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n # #region persist_tasks [TYPE Function] [C:3]\n # @PURPOSE: Persists multiple tasks.\n # @PRE isinstance(tasks, list)\n # @POST All tasks in list are persisted.\n # @PARAM tasks (List[Task]) - The list of tasks to persist.\n # @RELATION CALLS -> [persist_task]\n def persist_tasks(self, tasks: list[Task]) -> None:\n with belief_scope(\"TaskPersistenceService.persist_tasks\"):\n for task in tasks:\n self.persist_task(task)\n # #endregion persist_tasks\n # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.now(UTC)\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n # #region delete_tasks [TYPE Function] [C:3]\n # @PURPOSE: Deletes specific tasks from the database.\n # @PRE task_ids is a list of strings.\n # @POST Specified task records deleted from database.\n # @PARAM task_ids (List[str]) - List of task IDs to delete.\n # @SIDE_EFFECT Deletes rows from task_records table.\n # @RELATION DEPENDS_ON -> [TaskRecord]\n def delete_tasks(self, task_ids: list[str]) -> None:\n if not task_ids:\n return\n with belief_scope(\"TaskPersistenceService.delete_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n session.query(TaskRecord).filter(TaskRecord.id.in_(task_ids)).delete(\n synchronize_session=False\n )\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to delete tasks: {e}\")\n finally:\n session.close()\n # #endregion delete_tasks\n# #endregion TaskPersistenceService\n" }, { "contract_id": "_json_load_if_needed", @@ -27545,7 +28418,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.dict()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n" + "body": " # #region persist_task [TYPE Function] [C:3]\n # @PURPOSE: Persists or updates a single task in the database.\n # @PRE isinstance(task, Task)\n # @POST Task record created or updated in database.\n # @PARAM task (Task) - The task object to persist.\n # @SIDE_EFFECT Writes to task_records table in tasks.db\n # @DATA_CONTRACT Input[Task] -> Model[TaskRecord]\n # @RELATION CALLS -> [_resolve_environment_id]\n def persist_task(self, task: Task) -> None:\n with belief_scope(\"TaskPersistenceService.persist_task\", f\"task_id={task.id}\"):\n session: Session = TasksSessionLocal()\n try:\n record = (\n session.query(TaskRecord).filter(TaskRecord.id == task.id).first()\n )\n if not record:\n record = TaskRecord(id=task.id)\n session.add(record)\n record.type = task.plugin_id\n record.status = task.status.value\n raw_env_id = task.params.get(\"environment_id\") or task.params.get(\n \"source_env_id\"\n )\n record.environment_id = self._resolve_environment_id(\n session, raw_env_id\n )\n record.started_at = task.started_at\n record.finished_at = task.finished_at\n # Ensure params and result are JSON serializable\n def json_serializable(obj):\n # Hot-path utility — no entry/exit logging\n if isinstance(obj, dict):\n return {k: json_serializable(v) for k, v in obj.items()}\n elif isinstance(obj, list):\n return [json_serializable(v) for v in obj]\n elif isinstance(obj, datetime):\n return obj.isoformat()\n return obj\n record.params = json_serializable(task.params)\n record.result = json_serializable(task.result)\n # Store logs as JSON, converting datetime to string\n record.logs = []\n for log in task.logs:\n log_dict = log.model_dump()\n if isinstance(log_dict.get(\"timestamp\"), datetime):\n log_dict[\"timestamp\"] = log_dict[\"timestamp\"].isoformat()\n # Also clean up any datetimes in context\n if log_dict.get(\"context\"):\n log_dict[\"context\"] = json_serializable(log_dict[\"context\"])\n record.logs.append(log_dict)\n # Extract error if failed\n if task.status == TaskStatus.FAILED:\n for log in reversed(task.logs):\n if log.level == \"ERROR\":\n record.error = log.message\n break\n session.commit()\n except Exception as e:\n session.rollback()\n logger.error(f\"Failed to persist task {task.id}: {e}\")\n finally:\n session.close()\n # #endregion persist_task\n" }, { "contract_id": "persist_tasks", @@ -27609,7 +28482,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.utcnow()\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n" + "body": " # #region load_tasks [TYPE Function] [C:3]\n # @PURPOSE: Loads tasks from the database.\n # @PRE limit is an integer.\n # @POST Returns list of Task objects.\n # @PARAM limit (int) - Max tasks to load.\n # @PARAM status (Optional[TaskStatus]) - Filter by status.\n # @RETURN List[Task] - The loaded tasks.\n # @DATA_CONTRACT Model[TaskRecord] -> Output[List[Task]]\n # @RELATION CALLS -> [_json_load_if_needed]\n # @RELATION CALLS -> [_parse_datetime]\n def load_tasks(\n self, limit: int = 100, status: TaskStatus | None = None\n ) -> list[Task]:\n with belief_scope(\"TaskPersistenceService.load_tasks\"):\n session: Session = TasksSessionLocal()\n try:\n query = session.query(TaskRecord)\n if status:\n query = query.filter(TaskRecord.status == status.value)\n records = (\n query.order_by(TaskRecord.created_at.desc()).limit(limit).all()\n )\n loaded_tasks = []\n for record in records:\n try:\n logs = []\n logs_payload = self._json_load_if_needed(record.logs)\n if isinstance(logs_payload, list):\n for log_data in logs_payload:\n if not isinstance(log_data, dict):\n continue\n log_data = dict(log_data)\n log_data[\"timestamp\"] = (\n self._parse_datetime(log_data.get(\"timestamp\"))\n or datetime.now(UTC)\n )\n logs.append(LogEntry(**log_data))\n started_at = self._parse_datetime(record.started_at)\n finished_at = self._parse_datetime(record.finished_at)\n params = self._json_load_if_needed(record.params)\n result = self._json_load_if_needed(record.result)\n task = Task(\n id=record.id,\n plugin_id=record.type,\n status=TaskStatus(record.status),\n started_at=started_at,\n finished_at=finished_at,\n params=params if isinstance(params, dict) else {},\n result=result,\n logs=logs,\n )\n loaded_tasks.append(task)\n except Exception as e:\n logger.error(f\"Failed to reconstruct task {record.id}: {e}\")\n return loaded_tasks\n finally:\n session.close()\n # #endregion load_tasks\n" }, { "contract_id": "delete_tasks", @@ -29559,7 +30432,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS superset, preview, compile, execution, adapter]\n# @BRIEF Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context.\n# @LAYER Infrastructure\n# @RELATION CALLS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @PRE effective template params and dataset execution reference are available.\n# @POST preview and launch calls return Superset-originated artifacts or explicit errors.\n# @SIDE_EFFECT performs upstream Superset preview and SQL Lab calls.\n# @INVARIANT The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.\nfrom __future__ import annotations\n\n# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any\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\n\n# #endregion SupersetCompilationAdapter.imports\n# #region PreviewCompilationPayload [C:2] [TYPE Class]\n# @BRIEF Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: dict[str, Any]\n effective_filters: list[dict[str, Any]]\n# #endregion PreviewCompilationPayload\n# #region SqlLabLaunchPayload [C:2] [TYPE Class]\n# @BRIEF Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: dict[str, Any]\n# #endregion SqlLabLaunchPayload\n# #region SupersetCompilationAdapter [C:4] [TYPE Class]\n# @BRIEF Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication.\n# @RELATION CALLS -> [SupersetClient]\n# @PRE environment is configured and Superset is reachable for the target session.\n# @POST adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references.\n# @SIDE_EFFECT issues network requests to Superset API surfaces.\nclass SupersetCompilationAdapter:\n # #region SupersetCompilationAdapter.__init__ [TYPE Function] [C:2]\n # @PURPOSE: Bind adapter to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: SupersetClient | None = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n # #endregion SupersetCompilationAdapter.__init__\n # #region SupersetCompilationAdapter._supports_client_method [TYPE Function] [C:2]\n # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods.\n def _supports_client_method(self, method_name: str) -> bool:\n client_dict = getattr(self.client, \"__dict__\", {})\n if method_name in client_dict:\n return callable(client_dict[method_name])\n return callable(getattr(type(self.client), method_name, None))\n # #endregion SupersetCompilationAdapter._supports_client_method\n # #region SupersetCompilationAdapter.compile_preview [TYPE Function] [C:4]\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_superset_preview]\n # @PRE dataset_id and effective inputs are available for the current session.\n # @POST returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT performs upstream preview requests.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.utcnow(),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n # #endregion SupersetCompilationAdapter.compile_preview\n # #region SupersetCompilationAdapter.mark_preview_stale [TYPE Function] [C:2]\n # @PURPOSE: Invalidate previous preview after mapping or value changes.\n # @PRE preview is a persisted preview artifact or current in-memory snapshot.\n # @POST preview status becomes stale without fabricating a replacement artifact.\n def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview:\n preview.preview_status = PreviewStatus.STALE\n return preview\n # #endregion SupersetCompilationAdapter.mark_preview_stale\n # #region SupersetCompilationAdapter.create_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Create the canonical audited execution session after all launch gates pass.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_sql_lab_session]\n # @PRE compiled_sql is Superset-originated and launch gates are already satisfied.\n # @POST returns one canonical SQL Lab session reference from Superset.\n # @SIDE_EFFECT performs upstream SQL Lab execution/session creation.\n # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[str]\n def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str:\n with belief_scope(\"SupersetCompilationAdapter.create_sql_lab_session\"):\n compiled_sql = str(payload.compiled_sql or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"SQL Lab launch rejected because compiled SQL is empty\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise ValueError(\"compiled_sql must be non-empty\")\n logger.reason(\n \"Creating SQL Lab execution session from Superset-originated preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"preview_id\": payload.preview_id,\n },\n )\n result = self._request_sql_lab_session(payload)\n sql_lab_session_ref = str(\n result.get(\"sql_lab_session_ref\")\n or result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"result\", {}).get(\"id\")\n or \"\"\n ).strip()\n if not sql_lab_session_ref:\n logger.explore(\n \"Superset SQL Lab launch response did not include a stable session reference\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise RuntimeError(\n \"Superset SQL Lab launch response did not include a session reference\"\n )\n logger.reflect(\n \"Canonical SQL Lab session created successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n \"sql_lab_session_ref\": sql_lab_session_ref,\n },\n )\n return sql_lab_session_ref\n # #endregion SupersetCompilationAdapter.create_sql_lab_session\n # #region SupersetCompilationAdapter._request_superset_preview [TYPE Function] [C:4]\n # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.\n # @RELATION CALLS -> [EXT:method:SupersetClient.compile_dataset_preview]\n # @PRE payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.\n # @POST returns one normalized upstream compilation response including the chosen strategy metadata.\n # @SIDE_EFFECT issues one or more Superset preview requests through the client fallback chain.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]\n def _request_superset_preview(\n self, payload: PreviewCompilationPayload\n ) -> dict[str, Any]:\n direct_compile_preview = getattr(self.client, \"compile_preview\", None)\n if self._supports_client_method(\"compile_preview\") and callable(\n direct_compile_preview\n ):\n try:\n logger.reason(\n \"Attempting preview compilation via direct client capability\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n response = direct_compile_preview(payload)\n except TypeError:\n response = direct_compile_preview(\n payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Direct client preview capability failed; falling back to dataset preview strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n else:\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n direct_compile_dataset_preview = getattr(\n self.client, \"compile_dataset_preview\", None\n )\n if self._supports_client_method(\"compile_dataset_preview\") and callable(\n direct_compile_dataset_preview\n ):\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n response = direct_compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n if self._supports_client_method(\"compile_dataset_preview\"):\n response = self.client.compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n errors: list[str] = []\n for endpoint in (\n f\"/dataset/{payload.dataset_id}/preview\",\n f\"/dataset/{payload.dataset_id}/sql\",\n ):\n try:\n response = self.client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=self._dump_json(\n {\n \"template_params\": payload.template_params,\n \"effective_filters\": payload.effective_filters,\n }\n ),\n headers={\"Content-Type\": \"application/json\"},\n )\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n errors.append(f\"{endpoint}:unrecognized_response\")\n except Exception as exc:\n errors.append(f\"{endpoint}:{exc}\")\n raise RuntimeError(\n \"; \".join(errors) or \"Superset preview compilation failed\"\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n # #endregion SupersetCompilationAdapter._request_superset_preview\n # #region SupersetCompilationAdapter._request_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response.\n # @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n # @PRE payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.\n # @POST returns the first successful SQL Lab execution response from Superset.\n # @SIDE_EFFECT issues Superset dataset lookup and SQL Lab execution requests.\n # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]\n def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> dict[str, Any]:\n dataset_raw = self.client.get_dataset(payload.dataset_id)\n dataset_record = (\n dataset_raw.get(\"result\", dataset_raw)\n if isinstance(dataset_raw, dict)\n else {}\n )\n database_id = (\n dataset_record.get(\"database\", {}).get(\"id\")\n if isinstance(dataset_record.get(\"database\"), dict)\n else dataset_record.get(\"database_id\")\n )\n if database_id is None:\n raise RuntimeError(\n \"Superset dataset does not expose a database identifier for SQL Lab launch\"\n )\n request_payload = {\n \"database_id\": database_id,\n \"sql\": payload.compiled_sql,\n \"templateParams\": payload.template_params,\n \"schema\": dataset_record.get(\"schema\"),\n \"client_id\": payload.preview_id,\n }\n candidate_calls = [\n {\"kind\": \"network\", \"target\": \"/sqllab/execute/\", \"http_method\": \"POST\"},\n {\"kind\": \"network\", \"target\": \"/sql_lab/execute/\", \"http_method\": \"POST\"},\n ]\n errors: list[str] = []\n for candidate in candidate_calls:\n try:\n response = self.client.network.request(\n method=candidate[\"http_method\"],\n endpoint=candidate[\"target\"],\n data=self._dump_json(request_payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n return response\n except Exception as exc:\n errors.append(f\"{candidate['target']}:{exc}\")\n logger.explore(\n \"Superset SQL Lab candidate failed\",\n extra={\"target\": candidate[\"target\"], \"error\": str(exc)},\n )\n raise RuntimeError(\n \"; \".join(errors) or \"No Superset SQL Lab surface accepted the request\"\n )\n # #endregion SupersetCompilationAdapter._request_sql_lab_session\n # #region SupersetCompilationAdapter._normalize_preview_response [TYPE Function] [C:3]\n # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure.\n # @RELATION DEPENDS_ON -> [CompiledPreview]\n def _normalize_preview_response(self, response: Any) -> dict[str, Any] | None:\n if not isinstance(response, dict):\n return None\n compiled_sql_candidates = [\n response.get(\"compiled_sql\"),\n response.get(\"sql\"),\n response.get(\"query\"),\n ]\n result_payload = response.get(\"result\")\n if isinstance(result_payload, dict):\n compiled_sql_candidates.extend(\n [\n result_payload.get(\"compiled_sql\"),\n result_payload.get(\"sql\"),\n result_payload.get(\"query\"),\n ]\n )\n for candidate in compiled_sql_candidates:\n compiled_sql = str(candidate or \"\").strip()\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n }\n return None\n # #endregion SupersetCompilationAdapter._normalize_preview_response\n # #region SupersetCompilationAdapter._dump_json [TYPE Function] [C: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 return json.dumps(payload, sort_keys=True, default=str)\n # #endregion SupersetCompilationAdapter._dump_json\n# #endregion SupersetCompilationAdapter\n# #endregion SupersetCompilationAdapter\n" + "body": "# #region SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS superset, preview, compile, execution, adapter]\n# @BRIEF Interact with Superset preview compilation and SQL Lab execution endpoints using the current approved execution context.\n# @LAYER Infrastructure\n# @RELATION CALLS -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @PRE effective template params and dataset execution reference are available.\n# @POST preview and launch calls return Superset-originated artifacts or explicit errors.\n# @SIDE_EFFECT performs upstream Superset preview and SQL Lab calls.\n# @INVARIANT The adapter never fabricates compiled SQL locally; preview truth is delegated to Superset only.\nfrom __future__ import annotations\n\n# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import UTC, datetime\nfrom typing import Any\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\n\n# #endregion SupersetCompilationAdapter.imports\n# #region PreviewCompilationPayload [C:2] [TYPE Class]\n# @BRIEF Typed preview payload for Superset-side compilation.\n@dataclass(frozen=True)\nclass PreviewCompilationPayload:\n session_id: str\n dataset_id: int\n preview_fingerprint: str\n template_params: dict[str, Any]\n effective_filters: list[dict[str, Any]]\n# #endregion PreviewCompilationPayload\n# #region SqlLabLaunchPayload [C:2] [TYPE Class]\n# @BRIEF Typed SQL Lab payload for audited launch handoff.\n@dataclass(frozen=True)\nclass SqlLabLaunchPayload:\n session_id: str\n dataset_id: int\n preview_id: str\n compiled_sql: str\n template_params: dict[str, Any]\n# #endregion SqlLabLaunchPayload\n# #region SupersetCompilationAdapter [C:4] [TYPE Class]\n# @BRIEF Delegate preview compilation and SQL Lab launch to Superset without local SQL fabrication.\n# @RELATION CALLS -> [SupersetClient]\n# @PRE environment is configured and Superset is reachable for the target session.\n# @POST adapter can return explicit ready/failed preview artifacts and canonical SQL Lab references.\n# @SIDE_EFFECT issues network requests to Superset API surfaces.\nclass SupersetCompilationAdapter:\n # #region SupersetCompilationAdapter.__init__ [TYPE Function] [C:2]\n # @PURPOSE: Bind adapter to one Superset environment and client instance.\n def __init__(\n self, environment: Environment, client: SupersetClient | None = None\n ) -> None:\n self.environment = environment\n self.client = client or SupersetClient(environment)\n # #endregion SupersetCompilationAdapter.__init__\n # #region SupersetCompilationAdapter._supports_client_method [TYPE Function] [C:2]\n # @PURPOSE: Detect explicitly implemented client capabilities without treating loose mocks as real methods.\n def _supports_client_method(self, method_name: str) -> bool:\n client_dict = getattr(self.client, \"__dict__\", {})\n if method_name in client_dict:\n return callable(client_dict[method_name])\n return callable(getattr(type(self.client), method_name, None))\n # #endregion SupersetCompilationAdapter._supports_client_method\n # #region SupersetCompilationAdapter.compile_preview [TYPE Function] [C:4]\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_superset_preview]\n # @PRE dataset_id and effective inputs are available for the current session.\n # @POST returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT performs upstream preview requests.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(UTC),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n # #endregion SupersetCompilationAdapter.compile_preview\n # #region SupersetCompilationAdapter.mark_preview_stale [TYPE Function] [C:2]\n # @PURPOSE: Invalidate previous preview after mapping or value changes.\n # @PRE preview is a persisted preview artifact or current in-memory snapshot.\n # @POST preview status becomes stale without fabricating a replacement artifact.\n def mark_preview_stale(self, preview: CompiledPreview) -> CompiledPreview:\n preview.preview_status = PreviewStatus.STALE\n return preview\n # #endregion SupersetCompilationAdapter.mark_preview_stale\n # #region SupersetCompilationAdapter.create_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Create the canonical audited execution session after all launch gates pass.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_sql_lab_session]\n # @PRE compiled_sql is Superset-originated and launch gates are already satisfied.\n # @POST returns one canonical SQL Lab session reference from Superset.\n # @SIDE_EFFECT performs upstream SQL Lab execution/session creation.\n # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[str]\n def create_sql_lab_session(self, payload: SqlLabLaunchPayload) -> str:\n with belief_scope(\"SupersetCompilationAdapter.create_sql_lab_session\"):\n compiled_sql = str(payload.compiled_sql or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"SQL Lab launch rejected because compiled SQL is empty\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise ValueError(\"compiled_sql must be non-empty\")\n logger.reason(\n \"Creating SQL Lab execution session from Superset-originated preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"preview_id\": payload.preview_id,\n },\n )\n result = self._request_sql_lab_session(payload)\n sql_lab_session_ref = str(\n result.get(\"sql_lab_session_ref\")\n or result.get(\"query_id\")\n or result.get(\"id\")\n or result.get(\"result\", {}).get(\"id\")\n or \"\"\n ).strip()\n if not sql_lab_session_ref:\n logger.explore(\n \"Superset SQL Lab launch response did not include a stable session reference\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n },\n )\n raise RuntimeError(\n \"Superset SQL Lab launch response did not include a session reference\"\n )\n logger.reflect(\n \"Canonical SQL Lab session created successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"preview_id\": payload.preview_id,\n \"sql_lab_session_ref\": sql_lab_session_ref,\n },\n )\n return sql_lab_session_ref\n # #endregion SupersetCompilationAdapter.create_sql_lab_session\n # #region SupersetCompilationAdapter._request_superset_preview [TYPE Function] [C:4]\n # @PURPOSE: Request preview compilation through explicit client support backed by real Superset endpoints only.\n # @RELATION CALLS -> [EXT:method:SupersetClient.compile_dataset_preview]\n # @PRE payload contains a valid dataset identifier and deterministic execution inputs for one preview attempt.\n # @POST returns one normalized upstream compilation response including the chosen strategy metadata.\n # @SIDE_EFFECT issues one or more Superset preview requests through the client fallback chain.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[Dict[str,Any]]\n def _request_superset_preview(\n self, payload: PreviewCompilationPayload\n ) -> dict[str, Any]:\n direct_compile_preview = getattr(self.client, \"compile_preview\", None)\n if self._supports_client_method(\"compile_preview\") and callable(\n direct_compile_preview\n ):\n try:\n logger.reason(\n \"Attempting preview compilation via direct client capability\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n response = direct_compile_preview(payload)\n except TypeError:\n response = direct_compile_preview(\n payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Direct client preview capability failed; falling back to dataset preview strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n else:\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n direct_compile_dataset_preview = getattr(\n self.client, \"compile_dataset_preview\", None\n )\n if self._supports_client_method(\"compile_dataset_preview\") and callable(\n direct_compile_dataset_preview\n ):\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n response = direct_compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n try:\n logger.reason(\n \"Attempting deterministic Superset preview compilation through supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"filter_count\": len(payload.effective_filters),\n \"template_param_count\": len(payload.template_params),\n },\n )\n if self._supports_client_method(\"compile_dataset_preview\"):\n response = self.client.compile_dataset_preview(\n dataset_id=payload.dataset_id,\n template_params=payload.template_params,\n effective_filters=payload.effective_filters,\n )\n normalized = self._normalize_preview_response(response)\n if normalized is None:\n raise RuntimeError(\n \"Superset preview compilation response could not be normalized\"\n )\n return normalized\n errors: list[str] = []\n for endpoint in (\n f\"/dataset/{payload.dataset_id}/preview\",\n f\"/dataset/{payload.dataset_id}/sql\",\n ):\n try:\n response = self.client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=self._dump_json(\n {\n \"template_params\": payload.template_params,\n \"effective_filters\": payload.effective_filters,\n }\n ),\n headers={\"Content-Type\": \"application/json\"},\n )\n normalized = self._normalize_preview_response(response)\n if normalized is not None:\n return normalized\n errors.append(f\"{endpoint}:unrecognized_response\")\n except Exception as exc:\n errors.append(f\"{endpoint}:{exc}\")\n raise RuntimeError(\n \"; \".join(errors) or \"Superset preview compilation failed\"\n )\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed across supported endpoint strategies\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n \"error\": str(exc),\n },\n )\n raise RuntimeError(str(exc)) from exc\n # #endregion SupersetCompilationAdapter._request_superset_preview\n # #region SupersetCompilationAdapter._request_sql_lab_session [TYPE Function] [C:4]\n # @PURPOSE: Probe supported SQL Lab execution surfaces and return the first successful response.\n # @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]\n # @PRE payload carries non-empty Superset-originated SQL and a preview identifier for the current launch.\n # @POST returns the first successful SQL Lab execution response from Superset.\n # @SIDE_EFFECT issues Superset dataset lookup and SQL Lab execution requests.\n # @DATA_CONTRACT Input[SqlLabLaunchPayload] -> Output[Dict[str,Any]]\n def _request_sql_lab_session(self, payload: SqlLabLaunchPayload) -> dict[str, Any]:\n dataset_raw = self.client.get_dataset(payload.dataset_id)\n dataset_record = (\n dataset_raw.get(\"result\", dataset_raw)\n if isinstance(dataset_raw, dict)\n else {}\n )\n database_id = (\n dataset_record.get(\"database\", {}).get(\"id\")\n if isinstance(dataset_record.get(\"database\"), dict)\n else dataset_record.get(\"database_id\")\n )\n if database_id is None:\n raise RuntimeError(\n \"Superset dataset does not expose a database identifier for SQL Lab launch\"\n )\n request_payload = {\n \"database_id\": database_id,\n \"sql\": payload.compiled_sql,\n \"templateParams\": payload.template_params,\n \"schema\": dataset_record.get(\"schema\"),\n \"client_id\": payload.preview_id,\n }\n candidate_calls = [\n {\"kind\": \"network\", \"target\": \"/sqllab/execute/\", \"http_method\": \"POST\"},\n {\"kind\": \"network\", \"target\": \"/sql_lab/execute/\", \"http_method\": \"POST\"},\n ]\n errors: list[str] = []\n for candidate in candidate_calls:\n try:\n response = self.client.network.request(\n method=candidate[\"http_method\"],\n endpoint=candidate[\"target\"],\n data=self._dump_json(request_payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n return response\n except Exception as exc:\n errors.append(f\"{candidate['target']}:{exc}\")\n logger.explore(\n \"Superset SQL Lab candidate failed\",\n extra={\"target\": candidate[\"target\"], \"error\": str(exc)},\n )\n raise RuntimeError(\n \"; \".join(errors) or \"No Superset SQL Lab surface accepted the request\"\n )\n # #endregion SupersetCompilationAdapter._request_sql_lab_session\n # #region SupersetCompilationAdapter._normalize_preview_response [TYPE Function] [C:3]\n # @PURPOSE: Normalize candidate Superset preview responses into one compiled-sql structure.\n # @RELATION DEPENDS_ON -> [CompiledPreview]\n def _normalize_preview_response(self, response: Any) -> dict[str, Any] | None:\n if not isinstance(response, dict):\n return None\n compiled_sql_candidates = [\n response.get(\"compiled_sql\"),\n response.get(\"sql\"),\n response.get(\"query\"),\n ]\n result_payload = response.get(\"result\")\n if isinstance(result_payload, dict):\n compiled_sql_candidates.extend(\n [\n result_payload.get(\"compiled_sql\"),\n result_payload.get(\"sql\"),\n result_payload.get(\"query\"),\n ]\n )\n for candidate in compiled_sql_candidates:\n compiled_sql = str(candidate or \"\").strip()\n if compiled_sql:\n return {\n \"compiled_sql\": compiled_sql,\n \"raw_response\": response,\n }\n return None\n # #endregion SupersetCompilationAdapter._normalize_preview_response\n # #region SupersetCompilationAdapter._dump_json [TYPE Function] [C: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 return json.dumps(payload, sort_keys=True, default=str)\n # #endregion SupersetCompilationAdapter._dump_json\n# #endregion SupersetCompilationAdapter\n# #endregion SupersetCompilationAdapter\n" }, { "contract_id": "SupersetCompilationAdapter.imports", @@ -29574,7 +30447,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import Any\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\n\n# #endregion SupersetCompilationAdapter.imports\n" + "body": "# #region SupersetCompilationAdapter.imports [TYPE Block]\nfrom dataclasses import dataclass\nfrom datetime import UTC, datetime\nfrom typing import Any\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\n\n# #endregion SupersetCompilationAdapter.imports\n" }, { "contract_id": "PreviewCompilationPayload", @@ -29675,7 +30548,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region SupersetCompilationAdapter.compile_preview [TYPE Function] [C:4]\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_superset_preview]\n # @PRE dataset_id and effective inputs are available for the current session.\n # @POST returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT performs upstream preview requests.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.utcnow(),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n # #endregion SupersetCompilationAdapter.compile_preview\n" + "body": " # #region SupersetCompilationAdapter.compile_preview [TYPE Function] [C:4]\n # @PURPOSE: Request Superset-side compiled SQL preview for the current effective inputs.\n # @RELATION CALLS -> [SupersetCompilationAdapter._request_superset_preview]\n # @PRE dataset_id and effective inputs are available for the current session.\n # @POST returns a ready or failed preview artifact backed only by Superset-originated SQL or diagnostics.\n # @SIDE_EFFECT performs upstream preview requests.\n # @DATA_CONTRACT Input[PreviewCompilationPayload] -> Output[CompiledPreview]\n def compile_preview(self, payload: PreviewCompilationPayload) -> CompiledPreview:\n with belief_scope(\"SupersetCompilationAdapter.compile_preview\"):\n if payload.dataset_id <= 0:\n logger.explore(\n \"Preview compilation rejected because dataset identifier is invalid\",\n extra={\n \"dataset_id\": payload.dataset_id,\n \"session_id\": payload.session_id,\n },\n )\n raise ValueError(\"dataset_id must be a positive integer\")\n logger.reason(\n \"Requesting Superset-generated SQL preview\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"template_param_count\": len(payload.template_params),\n \"filter_count\": len(payload.effective_filters),\n },\n )\n try:\n preview_result = self._request_superset_preview(payload)\n except Exception as exc:\n logger.explore(\n \"Superset preview compilation failed with explicit upstream error\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"error\": str(exc),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_failed\",\n error_details=str(exc),\n compiled_at=None,\n )\n compiled_sql = str(preview_result.get(\"compiled_sql\") or \"\").strip()\n if not compiled_sql:\n logger.explore(\n \"Superset preview response did not include compiled SQL\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"response_keys\": sorted(preview_result.keys()),\n },\n )\n return CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.FAILED,\n compiled_sql=None,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=\"superset_preview_empty\",\n error_details=\"Superset preview response did not include compiled SQL\",\n compiled_at=None,\n )\n preview = CompiledPreview(\n session_id=payload.session_id,\n preview_status=PreviewStatus.READY,\n compiled_sql=compiled_sql,\n preview_fingerprint=payload.preview_fingerprint,\n compiled_by=\"superset\",\n error_code=None,\n error_details=None,\n compiled_at=datetime.now(UTC),\n )\n logger.reflect(\n \"Superset-generated SQL preview captured successfully\",\n extra={\n \"session_id\": payload.session_id,\n \"dataset_id\": payload.dataset_id,\n \"compiled_sql_length\": len(compiled_sql),\n },\n )\n return preview\n # #endregion SupersetCompilationAdapter.compile_preview\n" }, { "contract_id": "SupersetCompilationAdapter.mark_preview_stale", @@ -30691,7 +31564,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region WsLogHandlerModule [C:3] [TYPE Module] [SEMANTICS pydantic, dto, log-entry]\n# @BRIEF WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [CotJsonFormatter]\nfrom collections import deque\nfrom datetime import datetime\nimport logging\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #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: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #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\n # #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\n # #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\n\n# #endregion WebSocketLogHandler\n# #endregion WsLogHandlerModule\n" + "body": "# #region WsLogHandlerModule [C:3] [TYPE Module] [SEMANTICS pydantic, dto, log-entry]\n# @BRIEF WebSocket log handler module providing LogEntry DTO and WebSocketLogHandler for buffered log streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [CotJsonFormatter]\nfrom collections import deque\nfrom datetime import UTC, datetime\nimport logging\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LogEntry [C:1] [TYPE Class] [SEMANTICS log,entry,record,pydantic]\nclass LogEntry(BaseModel):\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n level: str\n message: str\n context: dict[str, Any] | None = None\n# #endregion LogEntry\n\n\n# #region WebSocketLogHandler [C:3] [TYPE Class] [SEMANTICS logging,handler,websocket,buffer]\n# @BRIEF Custom logging handler that captures log records into a fixed-capacity buffer for WebSocket streaming.\n# @RELATION DEPENDS_ON -> [LogEntry]\n# @RELATION DEPENDS_ON -> [CotJsonFormatter]\nclass WebSocketLogHandler(logging.Handler):\n \"\"\"\n A logging handler that stores log records and can be extended to send them\n over WebSockets.\n \"\"\"\n\n # #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\n # #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\n # #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\n\n# #endregion WebSocketLogHandler\n# #endregion WsLogHandlerModule\n" }, { "contract_id": "WebSocketLogHandler", @@ -30782,7 +31655,7 @@ "contract_type": "Module", "file_path": "backend/src/dependencies.py", "start_line": 1, - "end_line": 579, + "end_line": 577, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -30855,14 +31728,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AppDependencies [C:4] [TYPE Module] [SEMANTICS fastapi, schedule, auth, api_key]\n# @BRIEF Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.\n# @LAYER Core\n# @PURPOSE Provides shared instances to app and routers.\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 -> [APIKeyPrincipal]\n# @RELATION CALLS -> [get_api_key_principal]\n# @RELATION CALLS -> [info]\n\nimport json\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom fastapi import Depends, HTTPException, Request, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError\n\nfrom .core.auth.jwt import decode_token, is_token_blacklisted\nfrom .core.auth.repository import AuthRepository\nfrom .core.config_manager import ConfigManager\nfrom .core.database import get_auth_db, get_db, init_db\nfrom .core.logger import logger\nfrom .core.plugin_loader import PluginLoader\nfrom .core.scheduler import SchedulerService\nfrom .core.task_manager import TaskManager\nfrom .models.api_key import APIKey\nfrom .models.auth import User\nfrom .services.clean_release.facade import CleanReleaseFacade\nfrom .services.clean_release.repositories import (\n ApprovalRepository,\n ArtifactRepository,\n AuditRepository,\n CandidateRepository,\n ComplianceRepository,\n ManifestRepository,\n PolicyRepository,\n PublicationRepository,\n ReportRepository,\n)\nfrom .services.clean_release.repository import CleanReleaseRepository\nfrom .services.mapping_service import MappingService\nfrom .services.resource_service import ResourceService\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: ConfigManager | None = None\nplugin_loader: PluginLoader | None = None\ntask_manager: TaskManager | None = None\nscheduler_service: SchedulerService | None = None\nresource_service: ResourceService | None = None\n\n\n# #region get_config_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ConfigManager.\n# @PRE Global config_manager must be initialized.\n# @POST Returns shared ConfigManager 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# #endregion get_config_manager\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# #region get_plugin_loader [C:1] [TYPE Function]\n# @BRIEF Dependency injector for PluginLoader.\n# @PRE Global plugin_loader must be initialized.\n# @POST Returns shared PluginLoader 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 logger.info(f\"PluginLoader initialized with directory: {plugin_dir}\")\n logger.info(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# #endregion get_plugin_loader\n\n\n# #region get_task_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for TaskManager.\n# @PRE Global task_manager must be initialized.\n# @POST Returns shared TaskManager 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 logger.info(\"TaskManager initialized\")\n return task_manager\n\n\n# #endregion get_task_manager\n\n\n# #region get_scheduler_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for SchedulerService.\n# @PRE Global scheduler_service must be initialized.\n# @POST Returns shared SchedulerService 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 logger.info(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# #endregion get_scheduler_service\n\n\n# #region get_resource_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ResourceService.\n# @PRE Global resource_service must be initialized.\n# @POST Returns shared ResourceService 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 logger.info(\"ResourceService initialized\")\n return resource_service\n\n\n# #endregion get_resource_service\n\n\n# #region get_mapping_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for MappingService.\n# @PRE Global config_manager must be initialized.\n# @POST Returns new MappingService instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# #endregion get_mapping_service\n\n\n_clean_release_repository = CleanReleaseRepository()\n\n\n# #region get_clean_release_repository [C:1] [TYPE Function]\n# @BRIEF 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# #endregion get_clean_release_repository\n\n\n# #region get_clean_release_facade [C:1] [TYPE Function]\n# @BRIEF 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# #endregion get_clean_release_facade\n\n# #region APIKeyPrincipal [C:1] [TYPE Class]\n# @BRIEF Dataclass representing an authenticated API key principal for service-to-service auth.\n@dataclass\nclass APIKeyPrincipal:\n name: str\n api_key_id: str\n environment_id: Optional[str] = None\n permissions: list[str] = field(default_factory=list)\n# #endregion APIKeyPrincipal\n\n\n# #region require_api_key_or_jwt [C:4] [TYPE Function]\n# @BRIEF Factory: creates a dependency that accepts either a valid API key (with permission check)\n# OR a JWT (with standard has_permission check). Returns the authenticated user/principal name.\n# JWT takes precedence when both auth methods are present.\n# When API key is used, its environment_id scope is enforced against the caller's environment_id.\n# @PRE required_api_permission is a dot-separated string e.g. \"maintenance:start\".\n# required_jwt_resource and required_jwt_action are the JWT permission to check.\n# environment_id is the target environment for the operation (may be None for cross-env ops).\n# @POST Returns str (username or api key name). Raises 400/401/403 if auth fails or scope mismatch.\n# @SIDE_EFFECT Updates APIKey.last_used_at on successful API key auth.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [decode_token]\ndef require_api_key_or_jwt(\n required_api_permission: str,\n required_jwt_resource: str,\n required_jwt_action: str,\n environment_id: str | None = None,\n):\n \"\"\"Factory: creates a combined API-key-or-JWT auth dependency with environment scoping.\n\n JWT takes precedence when both Authorization: Bearer and X-API-Key are present.\n When API key auth is used, the key's environment_id scope is enforced:\n - If key.environment_id differs from environment_id → 400\n - If environment_id is None, the key's scope is used as default (auto-scope).\n\n Args:\n required_api_permission: Permission string to check against API key (e.g. 'maintenance:start').\n required_jwt_resource: JWT resource for has_permission fallback.\n required_jwt_action: JWT action for has_permission fallback.\n environment_id: Target environment for the operation. Pass None for cross-env operations.\n \"\"\"\n async def _auth_dependency(\n request: Request,\n db=Depends(get_db),\n auth_db=Depends(get_auth_db),\n token: str | None = Depends(oauth2_scheme_optional),\n ) -> str:\n from .core.auth.api_key import hash_api_key\n\n # Resolve effective environment_id: factory parameter > request body > None\n effective_env_id = environment_id\n if effective_env_id is None:\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except (json.JSONDecodeError, KeyError, AttributeError):\n pass\n\n # ── Try JWT auth first (JWT takes precedence over API key) ──\n if token:\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 HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n username = username_value\n except Exception:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n\n repo = AuthRepository(auth_db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"User not found\",\n )\n\n # Check JWT permission (Admin has full access)\n has_perm = any(\n role.name == \"Admin\"\n for role in user.roles\n )\n if not has_perm:\n for role in user.roles:\n for perm in role.permissions:\n if perm.resource == required_jwt_resource and perm.action == required_jwt_action:\n has_perm = True\n break\n if has_perm:\n break\n\n if not has_perm:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {required_jwt_resource}:{required_jwt_action}\",\n )\n\n logger.info(f\"JWT auth: {user.username}\")\n return f\"jwt:{user.username}\"\n\n # ── Try API key auth ──\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=timezone.utc)\n if expires_at < datetime.now(timezone.utc):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Check API key has required permission\n api_permissions = list(api_key.permissions or [])\n if required_api_permission not in api_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"API key lacks permission '{required_api_permission}'\",\n )\n\n # Environment scoping check: key's scope vs effective operation env\n key_env = api_key.environment_id\n if key_env is not None and effective_env_id is not None and key_env != effective_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n\n # Update last_used_at (best-effort, non-blocking)\n api_key.last_used_at = datetime.now(timezone.utc)\n db.flush()\n\n logger.info(f\"API key auth: {api_key.name} ({api_key.prefix}...)\")\n return f\"api_key:{api_key.name}\"\n\n # ── Neither JWT nor API key provided ──\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Authentication required (API key or JWT)\",\n )\n\n return _auth_dependency\n\n\n# #endregion require_api_key_or_jwt\n\n\n# #region check_api_key_environment_scope [C:1] [TYPE Function]\n# @BRIEF Check the request's API key environment scope against a target env_id. Raises 400 if mismatch.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def check_api_key_environment_scope(request: Request, db, target_env_id: str) -> None:\n \"\"\"Check the request's API key environment scope against a target environment_id.\n\n If the API key has an environment_id set and it differs from target_env_id,\n raises HTTP 400. This is used in route handlers where the target env_id\n is only known at request time (e.g., loaded from DB in end_maintenance).\n\n Args:\n request: FastAPI request object (to extract X-API-Key header).\n db: Database session.\n target_env_id: The target environment to validate against.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key is None:\n return\n\n key_env = api_key.environment_id\n if key_env is not None and key_env != target_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n# #endregion check_api_key_environment_scope\n\n\n# #region get_api_key_principal [C:3] [TYPE Function]\n# @BRIEF FastAPI dependency that reads X-API-Key header and returns APIKeyPrincipal or None.\n# @PRE X-API-Key header may be present in the request.\n# @POST If valid key: returns APIKeyPrincipal; if no header: returns None; if invalid: raises 401.\n# @SIDE_EFFECT Updates last_used_at on the APIKey row (non-blocking flush).\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def get_api_key_principal(\n request: Request,\n db=Depends(get_db),\n) -> Optional[APIKeyPrincipal]:\n \"\"\"Extract and validate API key from X-API-Key header.\n\n If no header is present, returns None (fall through to JWT auth).\n If header is present but invalid, raises 401.\n\n Returns:\n APIKeyPrincipal | None: Principal if valid API key, None if no header.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return None\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n\n if not api_key:\n logger.warning(f\"API key auth failed: key not found (hash prefix: {raw_key[:11]}...)\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n\n if not api_key.active:\n logger.warning(f\"API key auth failed: key revoked (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=timezone.utc)\n if expires_at < datetime.now(timezone.utc):\n logger.warning(f\"API key auth failed: key expired (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Update last_used_at (non-blocking, best-effort)\n api_key.last_used_at = datetime.now(timezone.utc)\n db.flush()\n\n logger.info(f\"API key auth success: {api_key.name} ({api_key.prefix}...)\")\n return APIKeyPrincipal(\n name=api_key.name,\n api_key_id=api_key.id,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n )\n\n\n# #endregion get_api_key_principal\n\n\n# #region oauth2_scheme [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [EXT:Library:OAuth2PasswordBearer]\n# @BRIEF OAuth2 password bearer scheme for token extraction (raises 401 on missing token).\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\", auto_error=True)\n# #endregion oauth2_scheme\n\n# #region oauth2_scheme_optional [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [EXT:Library:OAuth2PasswordBearer]\n# @BRIEF Optional OAuth2 scheme — returns None instead of raising 401 when no token.\n# @RATIONALE Used in dual-auth routes (API key OR JWT) where JWT may be absent.\noauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\", auto_error=False)\n# #endregion oauth2_scheme_optional\n\n\n# #region get_current_user [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @RELATION CALLS -> [is_token_blacklisted]\n# @BRIEF 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 and not revoked.\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 # Check blacklist\n if is_token_blacklisted(token, db):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Token has been revoked\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\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# #endregion get_current_user\n\n\n# #region has_permission [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for checking if the current user has a specific permission.\n# @PRE User is authenticated.\n# @POST Returns True if user has permission.\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) — uses is_admin flag, not name string.\n # Fallback to role.name == \"Admin\" for roles created before is_admin migration.\n if any(\n getattr(role, \"is_admin\", False) or role.name == \"Admin\"\n for role in current_user.roles\n ):\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# #endregion has_permission\n\n# #endregion AppDependencies\n" + "body": "# #region AppDependencies [C:4] [TYPE Module] [SEMANTICS fastapi, schedule, auth, api_key]\n# @BRIEF Manages creation and provision of shared application dependencies, such as PluginLoader and TaskManager, to avoid circular imports.\n# @LAYER Core\n# @PURPOSE Provides shared instances to app and routers.\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 -> [APIKeyPrincipal]\n# @RELATION CALLS -> [get_api_key_principal]\n# @RELATION CALLS -> [info]\n\nfrom dataclasses import dataclass, field\nfrom datetime import UTC, datetime\nimport json\nfrom pathlib import Path\n\nfrom fastapi import Depends, HTTPException, Request, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError\n\nfrom .core.auth.jwt import decode_token, is_token_blacklisted\nfrom .core.auth.repository import AuthRepository\nfrom .core.config_manager import ConfigManager\nfrom .core.database import get_auth_db, get_db, init_db\nfrom .core.logger import logger\nfrom .core.plugin_loader import PluginLoader\nfrom .core.scheduler import SchedulerService\nfrom .core.task_manager import TaskManager\nfrom .models.api_key import APIKey\nfrom .models.auth import User\nfrom .services.clean_release.facade import CleanReleaseFacade\nfrom .services.clean_release.repositories import (\n ApprovalRepository,\n ArtifactRepository,\n AuditRepository,\n CandidateRepository,\n ComplianceRepository,\n ManifestRepository,\n PolicyRepository,\n PublicationRepository,\n ReportRepository,\n)\nfrom .services.clean_release.repository import CleanReleaseRepository\nfrom .services.mapping_service import MappingService\nfrom .services.resource_service import ResourceService\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: ConfigManager | None = None\nplugin_loader: PluginLoader | None = None\ntask_manager: TaskManager | None = None\nscheduler_service: SchedulerService | None = None\nresource_service: ResourceService | None = None\n\n\n# #region get_config_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ConfigManager.\n# @PRE Global config_manager must be initialized.\n# @POST Returns shared ConfigManager 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# #endregion get_config_manager\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# #region get_plugin_loader [C:1] [TYPE Function]\n# @BRIEF Dependency injector for PluginLoader.\n# @PRE Global plugin_loader must be initialized.\n# @POST Returns shared PluginLoader 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 logger.info(f\"PluginLoader initialized with directory: {plugin_dir}\")\n logger.info(\n f\"Available plugins: {[config.name for config in plugin_loader.get_all_plugin_configs()]}\"\n )\n return plugin_loader\n\n\n# #endregion get_plugin_loader\n\n\n# #region get_task_manager [C:1] [TYPE Function]\n# @BRIEF Dependency injector for TaskManager.\n# @PRE Global task_manager must be initialized.\n# @POST Returns shared TaskManager 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 logger.info(\"TaskManager initialized\")\n return task_manager\n\n\n# #endregion get_task_manager\n\n\n# #region get_scheduler_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for SchedulerService.\n# @PRE Global scheduler_service must be initialized.\n# @POST Returns shared SchedulerService 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 logger.info(\"SchedulerService initialized\")\n return scheduler_service\n\n\n# #endregion get_scheduler_service\n\n\n# #region get_resource_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for ResourceService.\n# @PRE Global resource_service must be initialized.\n# @POST Returns shared ResourceService 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 logger.info(\"ResourceService initialized\")\n return resource_service\n\n\n# #endregion get_resource_service\n\n\n# #region get_mapping_service [C:1] [TYPE Function]\n# @BRIEF Dependency injector for MappingService.\n# @PRE Global config_manager must be initialized.\n# @POST Returns new MappingService instance.\ndef get_mapping_service() -> MappingService:\n \"\"\"Dependency injector for MappingService.\"\"\"\n return MappingService(get_config_manager())\n\n\n# #endregion get_mapping_service\n\n\n_clean_release_repository = CleanReleaseRepository()\n\n\n# #region get_clean_release_repository [C:1] [TYPE Function]\n# @BRIEF 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# #endregion get_clean_release_repository\n\n\n# #region get_clean_release_facade [C:1] [TYPE Function]\n# @BRIEF 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# #endregion get_clean_release_facade\n\n# #region APIKeyPrincipal [C:1] [TYPE Class]\n# @BRIEF Dataclass representing an authenticated API key principal for service-to-service auth.\n@dataclass\nclass APIKeyPrincipal:\n name: str\n api_key_id: str\n environment_id: str | None = None\n permissions: list[str] = field(default_factory=list)\n# #endregion APIKeyPrincipal\n\n\n# #region require_api_key_or_jwt [C:4] [TYPE Function]\n# @BRIEF Factory: creates a dependency that accepts either a valid API key (with permission check)\n# OR a JWT (with standard has_permission check). Returns the authenticated user/principal name.\n# JWT takes precedence when both auth methods are present.\n# When API key is used, its environment_id scope is enforced against the caller's environment_id.\n# @PRE required_api_permission is a dot-separated string e.g. \"maintenance:start\".\n# required_jwt_resource and required_jwt_action are the JWT permission to check.\n# environment_id is the target environment for the operation (may be None for cross-env ops).\n# @POST Returns str (username or api key name). Raises 400/401/403 if auth fails or scope mismatch.\n# @SIDE_EFFECT Updates APIKey.last_used_at on successful API key auth.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [decode_token]\ndef require_api_key_or_jwt(\n required_api_permission: str,\n required_jwt_resource: str,\n required_jwt_action: str,\n environment_id: str | None = None,\n):\n \"\"\"Factory: creates a combined API-key-or-JWT auth dependency with environment scoping.\n\n JWT takes precedence when both Authorization: Bearer and X-API-Key are present.\n When API key auth is used, the key's environment_id scope is enforced:\n - If key.environment_id differs from environment_id → 400\n - If environment_id is None, the key's scope is used as default (auto-scope).\n\n Args:\n required_api_permission: Permission string to check against API key (e.g. 'maintenance:start').\n required_jwt_resource: JWT resource for has_permission fallback.\n required_jwt_action: JWT action for has_permission fallback.\n environment_id: Target environment for the operation. Pass None for cross-env operations.\n \"\"\"\n async def _auth_dependency(\n request: Request,\n db=Depends(get_db),\n auth_db=Depends(get_auth_db),\n token: str | None = Depends(oauth2_scheme_optional),\n ) -> str:\n from .core.auth.api_key import hash_api_key\n\n # Resolve effective environment_id: factory parameter > request body > None\n effective_env_id = environment_id\n if effective_env_id is None:\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except (json.JSONDecodeError, KeyError, AttributeError):\n pass\n\n # ── Try JWT auth first (JWT takes precedence over API key) ──\n if token:\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 HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n username = username_value\n except Exception:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n\n repo = AuthRepository(auth_db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"User not found\",\n )\n\n # Check JWT permission (Admin has full access)\n has_perm = any(\n role.name == \"Admin\"\n for role in user.roles\n )\n if not has_perm:\n for role in user.roles:\n for perm in role.permissions:\n if perm.resource == required_jwt_resource and perm.action == required_jwt_action:\n has_perm = True\n break\n if has_perm:\n break\n\n if not has_perm:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {required_jwt_resource}:{required_jwt_action}\",\n )\n\n logger.info(f\"JWT auth: {user.username}\")\n return f\"jwt:{user.username}\"\n\n # ── Try API key auth ──\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=UTC)\n if expires_at < datetime.now(UTC):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Check API key has required permission\n api_permissions = list(api_key.permissions or [])\n if required_api_permission not in api_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"API key lacks permission '{required_api_permission}'\",\n )\n\n # Environment scoping check: key's scope vs effective operation env\n key_env = api_key.environment_id\n if key_env is not None and effective_env_id is not None and key_env != effective_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n\n # Update last_used_at (best-effort, non-blocking)\n api_key.last_used_at = datetime.now(UTC)\n db.flush()\n\n logger.info(f\"API key auth: {api_key.name} ({api_key.prefix}...)\")\n return f\"api_key:{api_key.name}\"\n\n # ── Neither JWT nor API key provided ──\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Authentication required (API key or JWT)\",\n )\n\n return _auth_dependency\n\n\n# #endregion require_api_key_or_jwt\n\n\n# #region check_api_key_environment_scope [C:1] [TYPE Function]\n# @BRIEF Check the request's API key environment scope against a target env_id. Raises 400 if mismatch.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def check_api_key_environment_scope(request: Request, db, target_env_id: str) -> None:\n \"\"\"Check the request's API key environment scope against a target environment_id.\n\n If the API key has an environment_id set and it differs from target_env_id,\n raises HTTP 400. This is used in route handlers where the target env_id\n is only known at request time (e.g., loaded from DB in end_maintenance).\n\n Args:\n request: FastAPI request object (to extract X-API-Key header).\n db: Database session.\n target_env_id: The target environment to validate against.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if api_key is None:\n return\n\n key_env = api_key.environment_id\n if key_env is not None and key_env != target_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n# #endregion check_api_key_environment_scope\n\n\n# #region get_api_key_principal [C:3] [TYPE Function]\n# @BRIEF FastAPI dependency that reads X-API-Key header and returns APIKeyPrincipal or None.\n# @PRE X-API-Key header may be present in the request.\n# @POST If valid key: returns APIKeyPrincipal; if no header: returns None; if invalid: raises 401.\n# @SIDE_EFFECT Updates last_used_at on the APIKey row (non-blocking flush).\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def get_api_key_principal(\n request: Request,\n db=Depends(get_db),\n) -> APIKeyPrincipal | None:\n \"\"\"Extract and validate API key from X-API-Key header.\n\n If no header is present, returns None (fall through to JWT auth).\n If header is present but invalid, raises 401.\n\n Returns:\n APIKeyPrincipal | None: Principal if valid API key, None if no header.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return None\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n\n if not api_key:\n logger.warning(f\"API key auth failed: key not found (hash prefix: {raw_key[:11]}...)\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n\n if not api_key.active:\n logger.warning(f\"API key auth failed: key revoked (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=UTC)\n if expires_at < datetime.now(UTC):\n logger.warning(f\"API key auth failed: key expired (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Update last_used_at (non-blocking, best-effort)\n api_key.last_used_at = datetime.now(UTC)\n db.flush()\n\n logger.info(f\"API key auth success: {api_key.name} ({api_key.prefix}...)\")\n return APIKeyPrincipal(\n name=api_key.name,\n api_key_id=api_key.id,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n )\n\n\n# #endregion get_api_key_principal\n\n\n# #region oauth2_scheme [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [EXT:Library:OAuth2PasswordBearer]\n# @BRIEF OAuth2 password bearer scheme for token extraction (raises 401 on missing token).\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\", auto_error=True)\n# #endregion oauth2_scheme\n\n# #region oauth2_scheme_optional [C:1] [TYPE Variable]\n# @RELATION DEPENDS_ON -> [EXT:Library:OAuth2PasswordBearer]\n# @BRIEF Optional OAuth2 scheme — returns None instead of raising 401 when no token.\n# @RATIONALE Used in dual-auth routes (API key OR JWT) where JWT may be absent.\noauth2_scheme_optional = OAuth2PasswordBearer(tokenUrl=\"/api/auth/login\", auto_error=False)\n# #endregion oauth2_scheme_optional\n\n\n# #region get_current_user [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @RELATION CALLS -> [is_token_blacklisted]\n# @BRIEF 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 and not revoked.\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 # Check blacklist\n if is_token_blacklisted(token, db):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Token has been revoked\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\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# #endregion get_current_user\n\n\n# #region has_permission [C:3] [TYPE Function]\n# @RELATION CALLS -> AuthRepository\n# @BRIEF Dependency for checking if the current user has a specific permission.\n# @PRE User is authenticated.\n# @POST Returns True if user has permission.\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) — uses is_admin flag, not name string.\n # Fallback to role.name == \"Admin\" for roles created before is_admin migration.\n if any(\n getattr(role, \"is_admin\", False) or role.name == \"Admin\"\n for role in current_user.roles\n ):\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# #endregion has_permission\n\n# #endregion AppDependencies\n" }, { "contract_id": "get_config_manager", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 65, - "end_line": 78, + "start_line": 63, + "end_line": 76, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30881,8 +31754,8 @@ "contract_id": "get_plugin_loader", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 88, - "end_line": 104, + "start_line": 86, + "end_line": 102, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30901,8 +31774,8 @@ "contract_id": "get_task_manager", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 107, - "end_line": 120, + "start_line": 105, + "end_line": 118, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30921,8 +31794,8 @@ "contract_id": "get_scheduler_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 123, - "end_line": 136, + "start_line": 121, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30941,8 +31814,8 @@ "contract_id": "get_resource_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 139, - "end_line": 152, + "start_line": 137, + "end_line": 150, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30961,8 +31834,8 @@ "contract_id": "get_mapping_service", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 155, - "end_line": 164, + "start_line": 153, + "end_line": 162, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -30981,8 +31854,8 @@ "contract_id": "get_clean_release_repository", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 170, - "end_line": 178, + "start_line": 168, + "end_line": 176, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31000,8 +31873,8 @@ "contract_id": "get_clean_release_facade", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 181, - "end_line": 209, + "start_line": 179, + "end_line": 207, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31019,8 +31892,8 @@ "contract_id": "APIKeyPrincipal", "contract_type": "Class", "file_path": "backend/src/dependencies.py", - "start_line": 211, - "end_line": 219, + "start_line": 209, + "end_line": 217, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31031,14 +31904,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region APIKeyPrincipal [C:1] [TYPE Class]\n# @BRIEF Dataclass representing an authenticated API key principal for service-to-service auth.\n@dataclass\nclass APIKeyPrincipal:\n name: str\n api_key_id: str\n environment_id: Optional[str] = None\n permissions: list[str] = field(default_factory=list)\n# #endregion APIKeyPrincipal\n" + "body": "# #region APIKeyPrincipal [C:1] [TYPE Class]\n# @BRIEF Dataclass representing an authenticated API key principal for service-to-service auth.\n@dataclass\nclass APIKeyPrincipal:\n name: str\n api_key_id: str\n environment_id: str | None = None\n permissions: list[str] = field(default_factory=list)\n# #endregion APIKeyPrincipal\n" }, { "contract_id": "require_api_key_or_jwt", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 222, - "end_line": 379, + "start_line": 220, + "end_line": 377, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -31049,14 +31922,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region require_api_key_or_jwt [C:4] [TYPE Function]\n# @BRIEF Factory: creates a dependency that accepts either a valid API key (with permission check)\n# OR a JWT (with standard has_permission check). Returns the authenticated user/principal name.\n# JWT takes precedence when both auth methods are present.\n# When API key is used, its environment_id scope is enforced against the caller's environment_id.\n# @PRE required_api_permission is a dot-separated string e.g. \"maintenance:start\".\n# required_jwt_resource and required_jwt_action are the JWT permission to check.\n# environment_id is the target environment for the operation (may be None for cross-env ops).\n# @POST Returns str (username or api key name). Raises 400/401/403 if auth fails or scope mismatch.\n# @SIDE_EFFECT Updates APIKey.last_used_at on successful API key auth.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [decode_token]\ndef require_api_key_or_jwt(\n required_api_permission: str,\n required_jwt_resource: str,\n required_jwt_action: str,\n environment_id: str | None = None,\n):\n \"\"\"Factory: creates a combined API-key-or-JWT auth dependency with environment scoping.\n\n JWT takes precedence when both Authorization: Bearer and X-API-Key are present.\n When API key auth is used, the key's environment_id scope is enforced:\n - If key.environment_id differs from environment_id → 400\n - If environment_id is None, the key's scope is used as default (auto-scope).\n\n Args:\n required_api_permission: Permission string to check against API key (e.g. 'maintenance:start').\n required_jwt_resource: JWT resource for has_permission fallback.\n required_jwt_action: JWT action for has_permission fallback.\n environment_id: Target environment for the operation. Pass None for cross-env operations.\n \"\"\"\n async def _auth_dependency(\n request: Request,\n db=Depends(get_db),\n auth_db=Depends(get_auth_db),\n token: str | None = Depends(oauth2_scheme_optional),\n ) -> str:\n from .core.auth.api_key import hash_api_key\n\n # Resolve effective environment_id: factory parameter > request body > None\n effective_env_id = environment_id\n if effective_env_id is None:\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except (json.JSONDecodeError, KeyError, AttributeError):\n pass\n\n # ── Try JWT auth first (JWT takes precedence over API key) ──\n if token:\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 HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n username = username_value\n except Exception:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n\n repo = AuthRepository(auth_db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"User not found\",\n )\n\n # Check JWT permission (Admin has full access)\n has_perm = any(\n role.name == \"Admin\"\n for role in user.roles\n )\n if not has_perm:\n for role in user.roles:\n for perm in role.permissions:\n if perm.resource == required_jwt_resource and perm.action == required_jwt_action:\n has_perm = True\n break\n if has_perm:\n break\n\n if not has_perm:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {required_jwt_resource}:{required_jwt_action}\",\n )\n\n logger.info(f\"JWT auth: {user.username}\")\n return f\"jwt:{user.username}\"\n\n # ── Try API key auth ──\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=timezone.utc)\n if expires_at < datetime.now(timezone.utc):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Check API key has required permission\n api_permissions = list(api_key.permissions or [])\n if required_api_permission not in api_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"API key lacks permission '{required_api_permission}'\",\n )\n\n # Environment scoping check: key's scope vs effective operation env\n key_env = api_key.environment_id\n if key_env is not None and effective_env_id is not None and key_env != effective_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n\n # Update last_used_at (best-effort, non-blocking)\n api_key.last_used_at = datetime.now(timezone.utc)\n db.flush()\n\n logger.info(f\"API key auth: {api_key.name} ({api_key.prefix}...)\")\n return f\"api_key:{api_key.name}\"\n\n # ── Neither JWT nor API key provided ──\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Authentication required (API key or JWT)\",\n )\n\n return _auth_dependency\n\n\n# #endregion require_api_key_or_jwt\n" + "body": "# #region require_api_key_or_jwt [C:4] [TYPE Function]\n# @BRIEF Factory: creates a dependency that accepts either a valid API key (with permission check)\n# OR a JWT (with standard has_permission check). Returns the authenticated user/principal name.\n# JWT takes precedence when both auth methods are present.\n# When API key is used, its environment_id scope is enforced against the caller's environment_id.\n# @PRE required_api_permission is a dot-separated string e.g. \"maintenance:start\".\n# required_jwt_resource and required_jwt_action are the JWT permission to check.\n# environment_id is the target environment for the operation (may be None for cross-env ops).\n# @POST Returns str (username or api key name). Raises 400/401/403 if auth fails or scope mismatch.\n# @SIDE_EFFECT Updates APIKey.last_used_at on successful API key auth.\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [decode_token]\ndef require_api_key_or_jwt(\n required_api_permission: str,\n required_jwt_resource: str,\n required_jwt_action: str,\n environment_id: str | None = None,\n):\n \"\"\"Factory: creates a combined API-key-or-JWT auth dependency with environment scoping.\n\n JWT takes precedence when both Authorization: Bearer and X-API-Key are present.\n When API key auth is used, the key's environment_id scope is enforced:\n - If key.environment_id differs from environment_id → 400\n - If environment_id is None, the key's scope is used as default (auto-scope).\n\n Args:\n required_api_permission: Permission string to check against API key (e.g. 'maintenance:start').\n required_jwt_resource: JWT resource for has_permission fallback.\n required_jwt_action: JWT action for has_permission fallback.\n environment_id: Target environment for the operation. Pass None for cross-env operations.\n \"\"\"\n async def _auth_dependency(\n request: Request,\n db=Depends(get_db),\n auth_db=Depends(get_auth_db),\n token: str | None = Depends(oauth2_scheme_optional),\n ) -> str:\n from .core.auth.api_key import hash_api_key\n\n # Resolve effective environment_id: factory parameter > request body > None\n effective_env_id = environment_id\n if effective_env_id is None:\n try:\n body = await request.json()\n effective_env_id = body.get(\"environment_id\")\n except (json.JSONDecodeError, KeyError, AttributeError):\n pass\n\n # ── Try JWT auth first (JWT takes precedence over API key) ──\n if token:\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 HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n username = username_value\n except Exception:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Could not validate credentials\",\n headers={\"WWW-Authenticate\": \"Bearer\"},\n )\n\n repo = AuthRepository(auth_db)\n user = repo.get_user_by_username(username)\n if user is None:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"User not found\",\n )\n\n # Check JWT permission (Admin has full access)\n has_perm = any(\n role.name == \"Admin\"\n for role in user.roles\n )\n if not has_perm:\n for role in user.roles:\n for perm in role.permissions:\n if perm.resource == required_jwt_resource and perm.action == required_jwt_action:\n has_perm = True\n break\n if has_perm:\n break\n\n if not has_perm:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"Permission denied for {required_jwt_resource}:{required_jwt_action}\",\n )\n\n logger.info(f\"JWT auth: {user.username}\")\n return f\"jwt:{user.username}\"\n\n # ── Try API key auth ──\n raw_key = request.headers.get(\"X-API-Key\")\n if raw_key:\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n if not api_key:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n if not api_key.active:\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=UTC)\n if expires_at < datetime.now(UTC):\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Check API key has required permission\n api_permissions = list(api_key.permissions or [])\n if required_api_permission not in api_permissions:\n raise HTTPException(\n status_code=status.HTTP_403_FORBIDDEN,\n detail=f\"API key lacks permission '{required_api_permission}'\",\n )\n\n # Environment scoping check: key's scope vs effective operation env\n key_env = api_key.environment_id\n if key_env is not None and effective_env_id is not None and key_env != effective_env_id:\n raise HTTPException(\n status_code=status.HTTP_400_BAD_REQUEST,\n detail=f\"API key is restricted to environment '{key_env}'\",\n )\n\n # Update last_used_at (best-effort, non-blocking)\n api_key.last_used_at = datetime.now(UTC)\n db.flush()\n\n logger.info(f\"API key auth: {api_key.name} ({api_key.prefix}...)\")\n return f\"api_key:{api_key.name}\"\n\n # ── Neither JWT nor API key provided ──\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Authentication required (API key or JWT)\",\n )\n\n return _auth_dependency\n\n\n# #endregion require_api_key_or_jwt\n" }, { "contract_id": "check_api_key_environment_scope", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 382, - "end_line": 415, + "start_line": 380, + "end_line": 413, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31086,8 +31959,8 @@ "contract_id": "get_api_key_principal", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 418, - "end_line": 484, + "start_line": 416, + "end_line": 482, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -31114,14 +31987,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region get_api_key_principal [C:3] [TYPE Function]\n# @BRIEF FastAPI dependency that reads X-API-Key header and returns APIKeyPrincipal or None.\n# @PRE X-API-Key header may be present in the request.\n# @POST If valid key: returns APIKeyPrincipal; if no header: returns None; if invalid: raises 401.\n# @SIDE_EFFECT Updates last_used_at on the APIKey row (non-blocking flush).\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def get_api_key_principal(\n request: Request,\n db=Depends(get_db),\n) -> Optional[APIKeyPrincipal]:\n \"\"\"Extract and validate API key from X-API-Key header.\n\n If no header is present, returns None (fall through to JWT auth).\n If header is present but invalid, raises 401.\n\n Returns:\n APIKeyPrincipal | None: Principal if valid API key, None if no header.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return None\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n\n if not api_key:\n logger.warning(f\"API key auth failed: key not found (hash prefix: {raw_key[:11]}...)\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n\n if not api_key.active:\n logger.warning(f\"API key auth failed: key revoked (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=timezone.utc)\n if expires_at < datetime.now(timezone.utc):\n logger.warning(f\"API key auth failed: key expired (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Update last_used_at (non-blocking, best-effort)\n api_key.last_used_at = datetime.now(timezone.utc)\n db.flush()\n\n logger.info(f\"API key auth success: {api_key.name} ({api_key.prefix}...)\")\n return APIKeyPrincipal(\n name=api_key.name,\n api_key_id=api_key.id,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n )\n\n\n# #endregion get_api_key_principal\n" + "body": "# #region get_api_key_principal [C:3] [TYPE Function]\n# @BRIEF FastAPI dependency that reads X-API-Key header and returns APIKeyPrincipal or None.\n# @PRE X-API-Key header may be present in the request.\n# @POST If valid key: returns APIKeyPrincipal; if no header: returns None; if invalid: raises 401.\n# @SIDE_EFFECT Updates last_used_at on the APIKey row (non-blocking flush).\n# @RELATION DEPENDS_ON -> [APIKeyModel]\n# @RELATION DEPENDS_ON -> [hash_api_key]\nasync def get_api_key_principal(\n request: Request,\n db=Depends(get_db),\n) -> APIKeyPrincipal | None:\n \"\"\"Extract and validate API key from X-API-Key header.\n\n If no header is present, returns None (fall through to JWT auth).\n If header is present but invalid, raises 401.\n\n Returns:\n APIKeyPrincipal | None: Principal if valid API key, None if no header.\n \"\"\"\n from .core.auth.api_key import hash_api_key\n\n raw_key = request.headers.get(\"X-API-Key\")\n if not raw_key:\n return None\n\n key_hash = hash_api_key(raw_key)\n api_key = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()\n\n if not api_key:\n logger.warning(f\"API key auth failed: key not found (hash prefix: {raw_key[:11]}...)\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"Invalid API key\",\n )\n\n if not api_key.active:\n logger.warning(f\"API key auth failed: key revoked (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has been revoked\",\n )\n\n if api_key.expires_at:\n expires_at = api_key.expires_at\n if expires_at.tzinfo is None:\n expires_at = expires_at.replace(tzinfo=UTC)\n if expires_at < datetime.now(UTC):\n logger.warning(f\"API key auth failed: key expired (id: {api_key.id}, name: {api_key.name})\")\n raise HTTPException(\n status_code=status.HTTP_401_UNAUTHORIZED,\n detail=\"API key has expired\",\n )\n\n # Update last_used_at (non-blocking, best-effort)\n api_key.last_used_at = datetime.now(UTC)\n db.flush()\n\n logger.info(f\"API key auth success: {api_key.name} ({api_key.prefix}...)\")\n return APIKeyPrincipal(\n name=api_key.name,\n api_key_id=api_key.id,\n environment_id=api_key.environment_id,\n permissions=list(api_key.permissions or []),\n )\n\n\n# #endregion get_api_key_principal\n" }, { "contract_id": "oauth2_scheme", "contract_type": "Variable", "file_path": "backend/src/dependencies.py", - "start_line": 487, - "end_line": 491, + "start_line": 485, + "end_line": 489, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31145,8 +32018,8 @@ "contract_id": "oauth2_scheme_optional", "contract_type": "Variable", "file_path": "backend/src/dependencies.py", - "start_line": 493, - "end_line": 498, + "start_line": 491, + "end_line": 496, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -31171,8 +32044,8 @@ "contract_id": "get_current_user", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 501, - "end_line": 537, + "start_line": 499, + "end_line": 535, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -31204,8 +32077,8 @@ "contract_id": "has_permission", "contract_type": "Function", "file_path": "backend/src/dependencies.py", - "start_line": 540, - "end_line": 577, + "start_line": 538, + "end_line": 575, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -31541,7 +32414,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_report_models [TYPE Module] [C:3] [SEMANTICS test, report, model, pydantic, validator]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Unit tests for report Pydantic models and their validators\n# @LAYER Domain\nfrom pathlib import Path\nimport sys\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\nfrom datetime import datetime, timedelta\nimport pytest\n\n\nclass TestTaskType:\n \"\"\"Tests for the TaskType enum.\"\"\"\n def test_enum_values(self):\n from src.models.report import TaskType\n assert TaskType.LLM_VERIFICATION == \"llm_verification\"\n assert TaskType.BACKUP == \"backup\"\n assert TaskType.MIGRATION == \"migration\"\n assert TaskType.DOCUMENTATION == \"documentation\"\n assert TaskType.UNKNOWN == \"unknown\"\nclass TestReportStatus:\n \"\"\"Tests for the ReportStatus enum.\"\"\"\n def test_enum_values(self):\n from src.models.report import ReportStatus\n assert ReportStatus.SUCCESS == \"success\"\n assert ReportStatus.FAILED == \"failed\"\n assert ReportStatus.IN_PROGRESS == \"in_progress\"\n assert ReportStatus.PARTIAL == \"partial\"\nclass TestErrorContext:\n \"\"\"Tests for ErrorContext model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Something failed\", code=\"ERR_001\", next_actions=[\"Retry\"])\n assert ctx.message == \"Something failed\"\n assert ctx.code == \"ERR_001\"\n assert ctx.next_actions == [\"Retry\"]\n def test_minimal_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Error occurred\")\n assert ctx.code is None\n assert ctx.next_actions == []\nclass TestTaskReport:\n \"\"\"Tests for TaskReport model and its validators.\"\"\"\n def _make_report(self, **overrides):\n from src.models.report import ReportStatus, TaskReport, TaskType\n defaults = {\n \"report_id\": \"rpt-001\",\n \"task_id\": \"task-001\",\n \"task_type\": TaskType.BACKUP,\n \"status\": ReportStatus.SUCCESS,\n \"updated_at\": datetime(2024, 1, 15, 12, 0, 0),\n \"summary\": \"Backup completed\",\n }\n defaults.update(overrides)\n return TaskReport(**defaults)\n def test_valid_creation(self):\n report = self._make_report()\n assert report.report_id == \"rpt-001\"\n assert report.task_id == \"task-001\"\n assert report.summary == \"Backup completed\"\n def test_empty_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\"\")\n def test_whitespace_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\" \")\n def test_empty_task_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(task_id=\"\")\n def test_empty_summary_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(summary=\"\")\n def test_summary_whitespace_trimmed(self):\n report = self._make_report(summary=\" Trimmed \")\n assert report.summary == \"Trimmed\"\n def test_optional_fields(self):\n report = self._make_report()\n assert report.started_at is None\n assert report.details is None\n assert report.error_context is None\n assert report.source_ref is None\n def test_with_error_context(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Connection failed\")\n report = self._make_report(error_context=ctx)\n assert report.error_context.message == \"Connection failed\"\nclass TestReportQuery:\n \"\"\"Tests for ReportQuery model and its validators.\"\"\"\n def test_defaults(self):\n from src.models.report import ReportQuery\n q = ReportQuery()\n assert q.page == 1\n assert q.page_size == 20\n assert q.task_types == []\n assert q.statuses == []\n assert q.sort_by == \"updated_at\"\n assert q.sort_order == \"desc\"\n def test_invalid_sort_by_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_by\"):\n ReportQuery(sort_by=\"invalid_field\")\n def test_valid_sort_by_values(self):\n from src.models.report import ReportQuery\n for field in [\"updated_at\", \"status\", \"task_type\"]:\n q = ReportQuery(sort_by=field)\n assert q.sort_by == field\n def test_invalid_sort_order_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_order\"):\n ReportQuery(sort_order=\"invalid\")\n def test_valid_sort_order_values(self):\n from src.models.report import ReportQuery\n for order in [\"asc\", \"desc\"]:\n q = ReportQuery(sort_order=order)\n assert q.sort_order == order\n def test_time_range_validation_valid(self):\n from src.models.report import ReportQuery\n now = datetime.utcnow()\n q = ReportQuery(time_from=now - timedelta(days=1), time_to=now)\n assert q.time_from < q.time_to\n def test_time_range_validation_invalid(self):\n from src.models.report import ReportQuery\n now = datetime.utcnow()\n with pytest.raises(ValueError, match=\"time_from\"):\n ReportQuery(time_from=now, time_to=now - timedelta(days=1))\n def test_page_ge_1(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page=0)\n def test_page_size_bounds(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page_size=0)\n with pytest.raises(ValueError):\n ReportQuery(page_size=101)\nclass TestReportCollection:\n \"\"\"Tests for ReportCollection model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ReportCollection, ReportQuery\n col = ReportCollection(\n items=[],\n total=0,\n page=1,\n page_size=20,\n has_next=False,\n applied_filters=ReportQuery(),\n )\n assert col.total == 0\n assert col.has_next is False\n def test_with_items(self):\n from src.models.report import ReportCollection, ReportQuery, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.utcnow(),\n summary=\"OK\"\n )\n col = ReportCollection(\n items=[report], total=1, page=1, page_size=20,\n has_next=False, applied_filters=ReportQuery()\n )\n assert len(col.items) == 1\n assert col.items[0].report_id == \"r1\"\nclass TestReportDetailView:\n \"\"\"Tests for ReportDetailView model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ReportDetailView, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.utcnow(),\n summary=\"Backup OK\"\n )\n detail = ReportDetailView(report=report)\n assert detail.report.report_id == \"r1\"\n assert detail.timeline == []\n assert detail.diagnostics is None\n assert detail.next_actions == []\n def test_with_all_fields(self):\n from src.models.report import ReportDetailView, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.MIGRATION,\n status=ReportStatus.FAILED, updated_at=datetime.utcnow(),\n summary=\"Migration failed\"\n )\n detail = ReportDetailView(\n report=report,\n timeline=[{\"event\": \"started\", \"at\": \"2024-01-01T00:00:00\"}],\n diagnostics={\"cause\": \"timeout\"},\n next_actions=[\"Retry\", \"Check connection\"],\n )\n assert len(detail.timeline) == 1\n assert detail.diagnostics[\"cause\"] == \"timeout\"\n assert \"Retry\" in detail.next_actions\n# #endregion test_report_models\n" + "body": "# #region test_report_models [TYPE Module] [C:3] [SEMANTICS test, report, model, pydantic, validator]\n# @RELATION BINDS_TO -> SrcRoot\n# @BRIEF Unit tests for report Pydantic models and their validators\n# @LAYER Domain\nfrom pathlib import Path\nimport sys\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\nfrom datetime import UTC, datetime, timedelta\nimport pytest\n\n\nclass TestTaskType:\n \"\"\"Tests for the TaskType enum.\"\"\"\n def test_enum_values(self):\n from src.models.report import TaskType\n assert TaskType.LLM_VERIFICATION == \"llm_verification\"\n assert TaskType.BACKUP == \"backup\"\n assert TaskType.MIGRATION == \"migration\"\n assert TaskType.DOCUMENTATION == \"documentation\"\n assert TaskType.UNKNOWN == \"unknown\"\nclass TestReportStatus:\n \"\"\"Tests for the ReportStatus enum.\"\"\"\n def test_enum_values(self):\n from src.models.report import ReportStatus\n assert ReportStatus.SUCCESS == \"success\"\n assert ReportStatus.FAILED == \"failed\"\n assert ReportStatus.IN_PROGRESS == \"in_progress\"\n assert ReportStatus.PARTIAL == \"partial\"\nclass TestErrorContext:\n \"\"\"Tests for ErrorContext model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Something failed\", code=\"ERR_001\", next_actions=[\"Retry\"])\n assert ctx.message == \"Something failed\"\n assert ctx.code == \"ERR_001\"\n assert ctx.next_actions == [\"Retry\"]\n def test_minimal_creation(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Error occurred\")\n assert ctx.code is None\n assert ctx.next_actions == []\nclass TestTaskReport:\n \"\"\"Tests for TaskReport model and its validators.\"\"\"\n def _make_report(self, **overrides):\n from src.models.report import ReportStatus, TaskReport, TaskType\n defaults = {\n \"report_id\": \"rpt-001\",\n \"task_id\": \"task-001\",\n \"task_type\": TaskType.BACKUP,\n \"status\": ReportStatus.SUCCESS,\n \"updated_at\": datetime(2024, 1, 15, 12, 0, 0),\n \"summary\": \"Backup completed\",\n }\n defaults.update(overrides)\n return TaskReport(**defaults)\n def test_valid_creation(self):\n report = self._make_report()\n assert report.report_id == \"rpt-001\"\n assert report.task_id == \"task-001\"\n assert report.summary == \"Backup completed\"\n def test_empty_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\"\")\n def test_whitespace_report_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(report_id=\" \")\n def test_empty_task_id_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(task_id=\"\")\n def test_empty_summary_raises(self):\n with pytest.raises(ValueError, match=\"non-empty\"):\n self._make_report(summary=\"\")\n def test_summary_whitespace_trimmed(self):\n report = self._make_report(summary=\" Trimmed \")\n assert report.summary == \"Trimmed\"\n def test_optional_fields(self):\n report = self._make_report()\n assert report.started_at is None\n assert report.details is None\n assert report.error_context is None\n assert report.source_ref is None\n def test_with_error_context(self):\n from src.models.report import ErrorContext\n ctx = ErrorContext(message=\"Connection failed\")\n report = self._make_report(error_context=ctx)\n assert report.error_context.message == \"Connection failed\"\nclass TestReportQuery:\n \"\"\"Tests for ReportQuery model and its validators.\"\"\"\n def test_defaults(self):\n from src.models.report import ReportQuery\n q = ReportQuery()\n assert q.page == 1\n assert q.page_size == 20\n assert q.task_types == []\n assert q.statuses == []\n assert q.sort_by == \"updated_at\"\n assert q.sort_order == \"desc\"\n def test_invalid_sort_by_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_by\"):\n ReportQuery(sort_by=\"invalid_field\")\n def test_valid_sort_by_values(self):\n from src.models.report import ReportQuery\n for field in [\"updated_at\", \"status\", \"task_type\"]:\n q = ReportQuery(sort_by=field)\n assert q.sort_by == field\n def test_invalid_sort_order_raises(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError, match=\"sort_order\"):\n ReportQuery(sort_order=\"invalid\")\n def test_valid_sort_order_values(self):\n from src.models.report import ReportQuery\n for order in [\"asc\", \"desc\"]:\n q = ReportQuery(sort_order=order)\n assert q.sort_order == order\n def test_time_range_validation_valid(self):\n from src.models.report import ReportQuery\n now = datetime.now(UTC)\n q = ReportQuery(time_from=now - timedelta(days=1), time_to=now)\n assert q.time_from < q.time_to\n def test_time_range_validation_invalid(self):\n from src.models.report import ReportQuery\n now = datetime.now(UTC)\n with pytest.raises(ValueError, match=\"time_from\"):\n ReportQuery(time_from=now, time_to=now - timedelta(days=1))\n def test_page_ge_1(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page=0)\n def test_page_size_bounds(self):\n from src.models.report import ReportQuery\n with pytest.raises(ValueError):\n ReportQuery(page_size=0)\n with pytest.raises(ValueError):\n ReportQuery(page_size=101)\nclass TestReportCollection:\n \"\"\"Tests for ReportCollection model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ReportCollection, ReportQuery\n col = ReportCollection(\n items=[],\n total=0,\n page=1,\n page_size=20,\n has_next=False,\n applied_filters=ReportQuery(),\n )\n assert col.total == 0\n assert col.has_next is False\n def test_with_items(self):\n from src.models.report import ReportCollection, ReportQuery, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.now(UTC),\n summary=\"OK\"\n )\n col = ReportCollection(\n items=[report], total=1, page=1, page_size=20,\n has_next=False, applied_filters=ReportQuery()\n )\n assert len(col.items) == 1\n assert col.items[0].report_id == \"r1\"\nclass TestReportDetailView:\n \"\"\"Tests for ReportDetailView model.\"\"\"\n def test_valid_creation(self):\n from src.models.report import ReportDetailView, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.BACKUP,\n status=ReportStatus.SUCCESS, updated_at=datetime.now(UTC),\n summary=\"Backup OK\"\n )\n detail = ReportDetailView(report=report)\n assert detail.report.report_id == \"r1\"\n assert detail.timeline == []\n assert detail.diagnostics is None\n assert detail.next_actions == []\n def test_with_all_fields(self):\n from src.models.report import ReportDetailView, ReportStatus, TaskReport, TaskType\n report = TaskReport(\n report_id=\"r1\", task_id=\"t1\", task_type=TaskType.MIGRATION,\n status=ReportStatus.FAILED, updated_at=datetime.now(UTC),\n summary=\"Migration failed\"\n )\n detail = ReportDetailView(\n report=report,\n timeline=[{\"event\": \"started\", \"at\": \"2024-01-01T00:00:00\"}],\n diagnostics={\"cause\": \"timeout\"},\n next_actions=[\"Retry\", \"Check connection\"],\n )\n assert len(detail.timeline) == 1\n assert detail.diagnostics[\"cause\"] == \"timeout\"\n assert \"Retry\" in detail.next_actions\n# #endregion test_report_models\n" }, { "contract_id": "APIKeyModel", @@ -31568,7 +32441,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region APIKeyModel [C:1] [TYPE Module] [SEMANTICS sqlalchemy, api_key, auth, model]\n# @BRIEF SQLAlchemy model for API Key authentication — stores SHA-256 hash only, raw key shown once at creation.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT key_hash is stored as SHA-256 hex digest (64 chars). prefix stores \"ssk_\" + first 7 chars.\n# @INVARIANT Raw key is NEVER stored — shown once at creation and discarded.\n\nimport uuid\nfrom datetime import datetime, timezone\n\nfrom sqlalchemy import Boolean, Column, DateTime, JSON, String\n\nfrom .mapping import Base\n\n\n# #region APIKey [C:1] [TYPE Class]\n# @BRIEF Stores API key metadata and SHA-256 hash for service-to-service authentication.\nclass APIKey(Base):\n __tablename__ = \"api_keys\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n key_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hex digest\n prefix = Column(String(11), nullable=False) # \"ssk_\" + 7 chars\n name = Column(String(255), nullable=False) # Human-readable label\n environment_id = Column(String(50), nullable=True) # Scoped to environment; null = all\n permissions = Column(JSON, nullable=False) # e.g. [\"maintenance:start\", \"maintenance:end\"]\n active = Column(Boolean, default=True, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)\n expires_at = Column(DateTime, nullable=True)\n last_used_at = Column(DateTime, nullable=True)\n\n def __repr__(self) -> str:\n return f\"\"\n\n# #endregion APIKey\n# #endregion APIKeyModel\n" + "body": "# #region APIKeyModel [C:1] [TYPE Module] [SEMANTICS sqlalchemy, api_key, auth, model]\n# @BRIEF SQLAlchemy model for API Key authentication — stores SHA-256 hash only, raw key shown once at creation.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT key_hash is stored as SHA-256 hex digest (64 chars). prefix stores \"ssk_\" + first 7 chars.\n# @INVARIANT Raw key is NEVER stored — shown once at creation and discarded.\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, String\n\nfrom .mapping import Base\n\n\n# #region APIKey [C:1] [TYPE Class]\n# @BRIEF Stores API key metadata and SHA-256 hash for service-to-service authentication.\nclass APIKey(Base):\n __tablename__ = \"api_keys\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n key_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hex digest\n prefix = Column(String(11), nullable=False) # \"ssk_\" + 7 chars\n name = Column(String(255), nullable=False) # Human-readable label\n environment_id = Column(String(50), nullable=True) # Scoped to environment; null = all\n permissions = Column(JSON, nullable=False) # e.g. [\"maintenance:start\", \"maintenance:end\"]\n active = Column(Boolean, default=True, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n expires_at = Column(DateTime, nullable=True)\n last_used_at = Column(DateTime, nullable=True)\n\n def __repr__(self) -> str:\n return f\"\"\n\n# #endregion APIKey\n# #endregion APIKeyModel\n" }, { "contract_id": "APIKey", @@ -31586,7 +32459,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region APIKey [C:1] [TYPE Class]\n# @BRIEF Stores API key metadata and SHA-256 hash for service-to-service authentication.\nclass APIKey(Base):\n __tablename__ = \"api_keys\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n key_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hex digest\n prefix = Column(String(11), nullable=False) # \"ssk_\" + 7 chars\n name = Column(String(255), nullable=False) # Human-readable label\n environment_id = Column(String(50), nullable=True) # Scoped to environment; null = all\n permissions = Column(JSON, nullable=False) # e.g. [\"maintenance:start\", \"maintenance:end\"]\n active = Column(Boolean, default=True, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), nullable=False)\n expires_at = Column(DateTime, nullable=True)\n last_used_at = Column(DateTime, nullable=True)\n\n def __repr__(self) -> str:\n return f\"\"\n\n# #endregion APIKey\n" + "body": "# #region APIKey [C:1] [TYPE Class]\n# @BRIEF Stores API key metadata and SHA-256 hash for service-to-service authentication.\nclass APIKey(Base):\n __tablename__ = \"api_keys\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n key_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hex digest\n prefix = Column(String(11), nullable=False) # \"ssk_\" + 7 chars\n name = Column(String(255), nullable=False) # Human-readable label\n environment_id = Column(String(50), nullable=True) # Scoped to environment; null = all\n permissions = Column(JSON, nullable=False) # e.g. [\"maintenance:start\", \"maintenance:end\"]\n active = Column(Boolean, default=True, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n expires_at = Column(DateTime, nullable=True)\n last_used_at = Column(DateTime, nullable=True)\n\n def __repr__(self) -> str:\n return f\"\"\n\n# #endregion APIKey\n" }, { "contract_id": "AssistantModels", @@ -31615,7 +32488,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, assistant, model, schema, audit, assistant-audit-record]\n# @BRIEF SQLAlchemy models for assistant audit trail and confirmation tokens.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @INVARIANT Assistant records preserve immutable ids and creation timestamps.\n# @SIDE_EFFECT Defines assistant audit/message/confirmation tables\n# @DATA_CONTRACT AssistantData -> AssistantRecord\n\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON, Column, DateTime, String, Text\n\nfrom .mapping import Base\n\n\n# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id must identify the actor for every record.\n# @POST Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantAuditRecord\n\n\n# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id, conversation_id, role and text must be present.\n# @POST Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantMessageRecord\n\n\n# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE intent/dispatch and expiry timestamp must be provided.\n# @POST State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n# #endregion AssistantModels\n" + "body": "# #region AssistantModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, assistant, model, schema, audit, assistant-audit-record]\n# @BRIEF SQLAlchemy models for assistant audit trail and confirmation tokens.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @INVARIANT Assistant records preserve immutable ids and creation timestamps.\n# @SIDE_EFFECT Defines assistant audit/message/confirmation tables\n# @DATA_CONTRACT AssistantData -> AssistantRecord\n\nfrom datetime import UTC, datetime\n\nfrom sqlalchemy import JSON, Column, DateTime, String, Text\n\nfrom .mapping import Base\n\n\n# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id must identify the actor for every record.\n# @POST Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n\n# #endregion AssistantAuditRecord\n\n\n# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id, conversation_id, role and text must be present.\n# @POST Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n\n# #endregion AssistantMessageRecord\n\n\n# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE intent/dispatch and expiry timestamp must be provided.\n# @POST State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n# #endregion AssistantModels\n" }, { "contract_id": "AssistantAuditRecord", @@ -31642,7 +32515,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id must identify the actor for every record.\n# @POST Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantAuditRecord\n" + "body": "# #region AssistantAuditRecord [C:3] [TYPE Class]\n# @BRIEF Store audit decisions and outcomes produced by assistant command handling.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id must identify the actor for every record.\n# @POST Audit payload remains available for compliance and debugging.\nclass AssistantAuditRecord(Base):\n __tablename__ = \"assistant_audit\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=True)\n decision = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n message = Column(Text, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n\n# #endregion AssistantAuditRecord\n" }, { "contract_id": "AssistantMessageRecord", @@ -31669,7 +32542,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id, conversation_id, role and text must be present.\n# @POST Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n\n# #endregion AssistantMessageRecord\n" + "body": "# #region AssistantMessageRecord [C:3] [TYPE Class]\n# @BRIEF Persist chat history entries for assistant conversations.\n# @RELATION INHERITS -> MappingModels\n# @PRE user_id, conversation_id, role and text must be present.\n# @POST Message row can be queried in chronological order.\nclass AssistantMessageRecord(Base):\n __tablename__ = \"assistant_messages\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n role = Column(String, nullable=False) # user | assistant\n text = Column(Text, nullable=False)\n state = Column(String, nullable=True)\n task_id = Column(String, nullable=True)\n confirmation_id = Column(String, nullable=True)\n payload = Column(JSON, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n\n# #endregion AssistantMessageRecord\n" }, { "contract_id": "AssistantConfirmationRecord", @@ -31696,7 +32569,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE intent/dispatch and expiry timestamp must be provided.\n# @POST State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n" + "body": "# #region AssistantConfirmationRecord [C:3] [TYPE Class]\n# @BRIEF Persist risky operation confirmation tokens with lifecycle state.\n# @RELATION INHERITS -> MappingModels\n# @PRE intent/dispatch and expiry timestamp must be provided.\n# @POST State transitions can be tracked and audited.\nclass AssistantConfirmationRecord(Base):\n __tablename__ = \"assistant_confirmations\"\n\n id = Column(String, primary_key=True)\n user_id = Column(String, index=True, nullable=False)\n conversation_id = Column(String, index=True, nullable=False)\n state = Column(String, index=True, nullable=False, default=\"pending\")\n intent = Column(JSON, nullable=False)\n dispatch = Column(JSON, nullable=False)\n expires_at = Column(DateTime, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n consumed_at = Column(DateTime, nullable=True)\n\n\n# #endregion AssistantConfirmationRecord\n" }, { "contract_id": "AuthModels", @@ -31727,7 +32600,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, model, schema, user]\n# @BRIEF SQLAlchemy models for multi-user authentication and authorization.\n# @LAYER Domain\n# @RELATION INHERITS -> [EXT:Library:SQLAlchemy.Base]\n#\n# @INVARIANT Usernames and emails must be unique.\n# @PRE Database engine initialized\n# @POST Auth ORM models registered with unique constraints\n# @SIDE_EFFECT Defines auth user tables\n# @DATA_CONTRACT UserData -> UserRecord\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Table\nfrom sqlalchemy.orm import relationship\n\nfrom .mapping import Base\n\n\n# #region generate_uuid [TYPE Function]\n# @BRIEF Generates a unique UUID string.\n# @POST Returns a string representation of a new UUID.\n# @RELATION DEPENDS_ON -> [EXT:Python:uuid]\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# #endregion generate_uuid\n\n# #region user_roles [TYPE Table]\n# @BRIEF Association table for many-to-many relationship between Users and Roles.\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# #endregion user_roles\n\n# #region role_permissions [TYPE Table]\n# @BRIEF Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\n# @RELATION DEPENDS_ON -> [Role]\n# @RELATION DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# #endregion role_permissions\n\n\n# #region User [TYPE Class]\n# @BRIEF Represents an identity that can authenticate to the system.\n# @RELATION BINDS_TO -> [Role]\nclass User(Base):\n __tablename__ = \"users\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n username = Column(String, unique=True, index=True, nullable=False)\n email = Column(String, unique=True, index=True, nullable=True)\n password_hash = Column(String, nullable=True)\n full_name = Column(String, nullable=True)\n auth_source = Column(String, default=\"LOCAL\") # LOCAL or ADFS\n is_active = Column(Boolean, default=True)\n is_ad_user = Column(Boolean, default=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n last_login = Column(DateTime, nullable=True)\n\n roles = relationship(\"Role\", secondary=user_roles, back_populates=\"users\")\n\n\n# #endregion User\n\n\n# #region Role [TYPE Class]\n# @BRIEF Represents a collection of permissions.\n# @RELATION BINDS_TO -> [User]\n# @RELATION BINDS_TO -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n is_admin = Column(Boolean, default=False, nullable=False)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# #endregion Role\n\n\n# #region Permission [TYPE Class]\n# @BRIEF Represents a specific capability within the system.\n# @RELATION BINDS_TO -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# #endregion Permission\n\n\n# #region TokenBlacklist [TYPE Class]\n# @BRIEF Stores SHA-256 hashes of revoked JWTs for server-side token blacklisting.\n# @INVARIANT Only the SHA-256 hash of the token is stored — never the raw token.\n# Expired entries are cleaned up by _prune_blacklist().\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\nclass TokenBlacklist(Base):\n __tablename__ = \"token_blacklist\"\n\n jti = Column(String, primary_key=True) # JWT ID from the token\n token_hash = Column(String(64), nullable=False, index=True) # SHA-256 of token\n expires_at = Column(DateTime, nullable=False) # Token's original exp claim\n revoked_at = Column(DateTime, nullable=False, default=datetime.utcnow)\n\n\n# #endregion TokenBlacklist\n\n# #region ADGroupMapping [TYPE Class]\n# @BRIEF Maps an Active Directory group to a local System Role.\n# @RELATION DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# #endregion ADGroupMapping\n\n# #endregion AuthModels\n" + "body": "# #region AuthModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, model, schema, user]\n# @BRIEF SQLAlchemy models for multi-user authentication and authorization.\n# @LAYER Domain\n# @RELATION INHERITS -> [EXT:Library:SQLAlchemy.Base]\n#\n# @INVARIANT Usernames and emails must be unique.\n# @PRE Database engine initialized\n# @POST Auth ORM models registered with unique constraints\n# @SIDE_EFFECT Defines auth user tables\n# @DATA_CONTRACT UserData -> UserRecord\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Table\nfrom sqlalchemy.orm import relationship\n\nfrom .mapping import Base\n\n\n# #region generate_uuid [TYPE Function]\n# @BRIEF Generates a unique UUID string.\n# @POST Returns a string representation of a new UUID.\n# @RELATION DEPENDS_ON -> [EXT:Python:uuid]\ndef generate_uuid():\n return str(uuid.uuid4())\n\n\n# #endregion generate_uuid\n\n# #region user_roles [TYPE Table]\n# @BRIEF Association table for many-to-many relationship between Users and Roles.\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nuser_roles = Table(\n \"user_roles\",\n Base.metadata,\n Column(\"user_id\", String, ForeignKey(\"users.id\"), primary_key=True),\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n)\n# #endregion user_roles\n\n# #region role_permissions [TYPE Table]\n# @BRIEF Association table for many-to-many relationship between Roles and Permissions.\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\n# @RELATION DEPENDS_ON -> [Role]\n# @RELATION DEPENDS_ON -> [Permission]\nrole_permissions = Table(\n \"role_permissions\",\n Base.metadata,\n Column(\"role_id\", String, ForeignKey(\"roles.id\"), primary_key=True),\n Column(\"permission_id\", String, ForeignKey(\"permissions.id\"), primary_key=True),\n)\n# #endregion role_permissions\n\n\n# #region User [TYPE Class]\n# @BRIEF Represents an identity that can authenticate to the system.\n# @RELATION BINDS_TO -> [Role]\nclass User(Base):\n __tablename__ = \"users\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n username = Column(String, unique=True, index=True, nullable=False)\n email = Column(String, unique=True, index=True, nullable=True)\n password_hash = Column(String, nullable=True)\n full_name = Column(String, nullable=True)\n auth_source = Column(String, default=\"LOCAL\") # LOCAL or ADFS\n is_active = Column(Boolean, default=True)\n is_ad_user = Column(Boolean, default=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n last_login = Column(DateTime, nullable=True)\n\n roles = relationship(\"Role\", secondary=user_roles, back_populates=\"users\")\n\n\n# #endregion User\n\n\n# #region Role [TYPE Class]\n# @BRIEF Represents a collection of permissions.\n# @RELATION BINDS_TO -> [User]\n# @RELATION BINDS_TO -> [Permission]\nclass Role(Base):\n __tablename__ = \"roles\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n name = Column(String, unique=True, index=True, nullable=False)\n description = Column(String, nullable=True)\n is_admin = Column(Boolean, default=False, nullable=False)\n\n users = relationship(\"User\", secondary=user_roles, back_populates=\"roles\")\n permissions = relationship(\n \"Permission\", secondary=role_permissions, back_populates=\"roles\"\n )\n\n\n# #endregion Role\n\n\n# #region Permission [TYPE Class]\n# @BRIEF Represents a specific capability within the system.\n# @RELATION BINDS_TO -> [Role]\nclass Permission(Base):\n __tablename__ = \"permissions\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n resource = Column(String, nullable=False) # e.g. \"plugin:backup\"\n action = Column(String, nullable=False) # e.g. \"READ\", \"EXECUTE\", \"WRITE\"\n\n roles = relationship(\n \"Role\", secondary=role_permissions, back_populates=\"permissions\"\n )\n\n\n# #endregion Permission\n\n\n# #region TokenBlacklist [TYPE Class]\n# @BRIEF Stores SHA-256 hashes of revoked JWTs for server-side token blacklisting.\n# @INVARIANT Only the SHA-256 hash of the token is stored — never the raw token.\n# Expired entries are cleaned up by _prune_blacklist().\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\nclass TokenBlacklist(Base):\n __tablename__ = \"token_blacklist\"\n\n jti = Column(String, primary_key=True) # JWT ID from the token\n token_hash = Column(String(64), nullable=False, index=True) # SHA-256 of token\n expires_at = Column(DateTime, nullable=False) # Token's original exp claim\n revoked_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n\n\n# #endregion TokenBlacklist\n\n# #region ADGroupMapping [TYPE Class]\n# @BRIEF Maps an Active Directory group to a local System Role.\n# @RELATION DEPENDS_ON -> [Role]\nclass ADGroupMapping(Base):\n __tablename__ = \"ad_group_mappings\"\n\n id = Column(String, primary_key=True, default=generate_uuid)\n ad_group = Column(String, unique=True, index=True, nullable=False)\n role_id = Column(String, ForeignKey(\"roles.id\"), nullable=False)\n\n role = relationship(\"Role\")\n\n\n# #endregion ADGroupMapping\n\n# #endregion AuthModels\n" }, { "contract_id": "generate_uuid", @@ -31896,7 +32769,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TokenBlacklist [TYPE Class]\n# @BRIEF Stores SHA-256 hashes of revoked JWTs for server-side token blacklisting.\n# @INVARIANT Only the SHA-256 hash of the token is stored — never the raw token.\n# Expired entries are cleaned up by _prune_blacklist().\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\nclass TokenBlacklist(Base):\n __tablename__ = \"token_blacklist\"\n\n jti = Column(String, primary_key=True) # JWT ID from the token\n token_hash = Column(String(64), nullable=False, index=True) # SHA-256 of token\n expires_at = Column(DateTime, nullable=False) # Token's original exp claim\n revoked_at = Column(DateTime, nullable=False, default=datetime.utcnow)\n\n\n# #endregion TokenBlacklist\n" + "body": "# #region TokenBlacklist [TYPE Class]\n# @BRIEF Stores SHA-256 hashes of revoked JWTs for server-side token blacklisting.\n# @INVARIANT Only the SHA-256 hash of the token is stored — never the raw token.\n# Expired entries are cleaned up by _prune_blacklist().\n# @RELATION DEPENDS_ON -> [EXT:Library:SQLAlchemy.Base]\nclass TokenBlacklist(Base):\n __tablename__ = \"token_blacklist\"\n\n jti = Column(String, primary_key=True) # JWT ID from the token\n token_hash = Column(String(64), nullable=False, index=True) # SHA-256 of token\n expires_at = Column(DateTime, nullable=False) # Token's original exp claim\n revoked_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n\n\n# #endregion TokenBlacklist\n" }, { "contract_id": "ADGroupMapping", @@ -31951,7 +32824,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanReleaseModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, clean-release, model, schema, schedule, release]\n# @BRIEF Define canonical clean release domain entities and lifecycle guards.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @PRE Base mapping model and release enums are available.\n# @POST Provides SQLAlchemy and dataclass definitions for governance domain.\n# @SIDE_EFFECT None (schema definition).\n# @DATA_CONTRACT Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]\n# @INVARIANT Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.\n\nfrom datetime import datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import ConfigDict, model_validator\nfrom pydantic.dataclasses import dataclass as pydantic_dataclass\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Integer, String\n\nfrom ..services.clean_release.enums import CandidateStatus, ClassificationType, ComplianceDecision, PublicationStatus, RunStatus\nfrom ..services.clean_release.exceptions import IllegalTransitionError\nfrom .mapping import Base\n\n\n# #region ExecutionMode [TYPE Class]\n# @BRIEF Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# #endregion ExecutionMode\n\n# #region CheckFinalStatus [TYPE Class]\n# @BRIEF 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# #endregion CheckFinalStatus\n\n# #region CheckStageName [TYPE Class]\n# @BRIEF 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# #endregion CheckStageName\n\n# #region CheckStageStatus [TYPE Class]\n# @BRIEF 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# #endregion CheckStageStatus\n\n# #region CheckStageResult [TYPE Class]\n# @BRIEF 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# #endregion CheckStageResult\n\n# #region ProfileType [TYPE Class]\n# @BRIEF Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# #endregion ProfileType\n\n# #region RegistryStatus [TYPE Class]\n# @BRIEF Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# #endregion RegistryStatus\n\n# #region ReleaseCandidateStatus [TYPE Class]\n# @BRIEF 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# #endregion ReleaseCandidateStatus\n\n# #region ResourceSourceEntry [TYPE Class]\n# @BRIEF 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# #endregion ResourceSourceEntry\n\n# #region ResourceSourceRegistry [TYPE Class]\n# @BRIEF 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: list[str] | None = None\n allowed_schemes: list[str] | None = None\n allowed_source_types: list[str] | None = 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# #endregion ResourceSourceRegistry\n\n# #region CleanProfilePolicy [TYPE Class]\n# @BRIEF 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: list[str] | None = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: dict[str, Any] | None = 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# #endregion CleanProfilePolicy\n\n# #region ComplianceCheckRun [TYPE Class]\n# @BRIEF 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: datetime | None = 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# #endregion ComplianceCheckRun\n\n# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE id, version, source_snapshot_ref are non-empty.\n# @POST status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n\n# #region CandidateArtifact [TYPE Class]\n# @BRIEF 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# #endregion CandidateArtifact\n\n# #region ManifestItem [TYPE 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: str | None = None\n# #endregion ManifestItem\n\n# #region ManifestSummary [TYPE 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# #endregion ManifestSummary\n\n# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n\n# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF 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# #endregion SourceRegistrySnapshot\n\n# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF 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# #endregion CleanPolicySnapshot\n\n# #region ComplianceRun [TYPE Class]\n# @BRIEF 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# #endregion ComplianceRun\n\n# #region ComplianceStageRun [TYPE Class]\n# @BRIEF 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# #endregion ComplianceStageRun\n\n# #region ViolationSeverity [TYPE Class]\n# @BRIEF Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# #endregion ViolationSeverity\n\n# #region ViolationCategory [TYPE Class]\n# @BRIEF 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# #endregion ViolationCategory\n\n# #region ComplianceViolation [TYPE Class]\n# @BRIEF 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) -> str | None:\n return self.artifact_path\n\n @property\n def remediation(self) -> str | None:\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# #endregion ComplianceViolation\n\n# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n\n# #region ApprovalDecision [TYPE Class]\n# @BRIEF 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# #endregion ApprovalDecision\n\n# #region PublicationRecord [TYPE Class]\n# @BRIEF 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# #endregion PublicationRecord\n\n# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\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# #endregion CleanReleaseAuditLog\n\n# #endregion CleanReleaseModels\n" + "body": "# #region CleanReleaseModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, clean-release, model, schema, schedule, release]\n# @BRIEF Define canonical clean release domain entities and lifecycle guards.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> MappingModels\n# @PRE Base mapping model and release enums are available.\n# @POST Provides SQLAlchemy and dataclass definitions for governance domain.\n# @SIDE_EFFECT None (schema definition).\n# @DATA_CONTRACT Model[ReleaseCandidate, CandidateArtifact, DistributionManifest, ComplianceRun, ComplianceReport]\n# @INVARIANT Immutable snapshots are never mutated; forbidden lifecycle transitions are rejected.\n\nfrom datetime import UTC, datetime\nfrom enum import Enum\nfrom typing import Any\n\nfrom pydantic import ConfigDict, model_validator\nfrom pydantic.dataclasses import dataclass as pydantic_dataclass\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, ForeignKey, Integer, String\n\nfrom ..services.clean_release.enums import CandidateStatus, ClassificationType, ComplianceDecision, PublicationStatus, RunStatus\nfrom ..services.clean_release.exceptions import IllegalTransitionError\nfrom .mapping import Base\n\n\n# #region ExecutionMode [TYPE Class]\n# @BRIEF Backward-compatible execution mode enum for legacy TUI/orchestrator tests.\nclass ExecutionMode(str, Enum):\n TUI = \"TUI\"\n API = \"API\"\n SCHEDULER = \"SCHEDULER\"\n# #endregion ExecutionMode\n\n# #region CheckFinalStatus [TYPE Class]\n# @BRIEF 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# #endregion CheckFinalStatus\n\n# #region CheckStageName [TYPE Class]\n# @BRIEF 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# #endregion CheckStageName\n\n# #region CheckStageStatus [TYPE Class]\n# @BRIEF 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# #endregion CheckStageStatus\n\n# #region CheckStageResult [TYPE Class]\n# @BRIEF 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# #endregion CheckStageResult\n\n# #region ProfileType [TYPE Class]\n# @BRIEF Backward-compatible profile enum for legacy TUI bootstrap logic.\nclass ProfileType(str, Enum):\n ENTERPRISE_CLEAN = \"enterprise-clean\"\n# #endregion ProfileType\n\n# #region RegistryStatus [TYPE Class]\n# @BRIEF Backward-compatible registry status enum for legacy TUI bootstrap logic.\nclass RegistryStatus(str, Enum):\n ACTIVE = \"ACTIVE\"\n INACTIVE = \"INACTIVE\"\n# #endregion RegistryStatus\n\n# #region ReleaseCandidateStatus [TYPE Class]\n# @BRIEF 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# #endregion ReleaseCandidateStatus\n\n# #region ResourceSourceEntry [TYPE Class]\n# @BRIEF 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# #endregion ResourceSourceEntry\n\n# #region ResourceSourceRegistry [TYPE Class]\n# @BRIEF 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: list[str] | None = None\n allowed_schemes: list[str] | None = None\n allowed_source_types: list[str] | None = 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# #endregion ResourceSourceRegistry\n\n# #region CleanProfilePolicy [TYPE Class]\n# @BRIEF 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: list[str] | None = None\n external_source_forbidden: bool = True\n immutable: bool = True\n content_json: dict[str, Any] | None = 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# #endregion CleanProfilePolicy\n\n# #region ComplianceCheckRun [TYPE Class]\n# @BRIEF 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: datetime | None = 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# #endregion ComplianceCheckRun\n\n# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE id, version, source_snapshot_ref are non-empty.\n# @POST status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n\n# #region CandidateArtifact [TYPE Class]\n# @BRIEF 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# #endregion CandidateArtifact\n\n# #region ManifestItem [TYPE 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: str | None = None\n# #endregion ManifestItem\n\n# #region ManifestSummary [TYPE 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# #endregion ManifestSummary\n\n# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n\n# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion SourceRegistrySnapshot\n\n# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion CleanPolicySnapshot\n\n# #region ComplianceRun [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion ComplianceRun\n\n# #region ComplianceStageRun [TYPE Class]\n# @BRIEF 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# #endregion ComplianceStageRun\n\n# #region ViolationSeverity [TYPE Class]\n# @BRIEF Backward-compatible violation severity enum for legacy clean-release tests.\nclass ViolationSeverity(str, Enum):\n CRITICAL = \"CRITICAL\"\n MAJOR = \"MAJOR\"\n MINOR = \"MINOR\"\n# #endregion ViolationSeverity\n\n# #region ViolationCategory [TYPE Class]\n# @BRIEF 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# #endregion ViolationCategory\n\n# #region ComplianceViolation [TYPE Class]\n# @BRIEF 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) -> str | None:\n return self.artifact_path\n\n @property\n def remediation(self) -> str | None:\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# #endregion ComplianceViolation\n\n# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=lambda: datetime.now(UTC))\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n\n# #region ApprovalDecision [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n comment = Column(String, nullable=True)\n# #endregion ApprovalDecision\n\n# #region PublicationRecord [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# #endregion PublicationRecord\n\n# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\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=lambda: datetime.now(UTC))\n details_json = Column(JSON, default=dict)\n# #endregion CleanReleaseAuditLog\n\n# #endregion CleanReleaseModels\n" }, { "contract_id": "ExecutionMode", @@ -32174,7 +33047,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE id, version, source_snapshot_ref are non-empty.\n# @POST status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n" + "body": "# #region ReleaseCandidate [TYPE Class]\n# @BRIEF Represents the release unit being prepared and governed.\n# @PRE id, version, source_snapshot_ref are non-empty.\n# @POST status advances only through legal transitions.\nclass ReleaseCandidate(Base):\n __tablename__ = \"clean_release_candidates\"\n\n id = Column(String, primary_key=True)\n name = Column(String, nullable=True) # Added back for backward compatibility with some legacy DTOs\n version = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n build_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n created_by = Column(String, nullable=False)\n status = Column(String, default=CandidateStatus.DRAFT)\n\n def __init__(self, **kwargs):\n if \"candidate_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"candidate_id\")\n if \"profile\" in kwargs:\n kwargs.pop(\"profile\")\n status = kwargs.get(\"status\")\n if status is None:\n kwargs[\"status\"] = CandidateStatus.DRAFT.value\n elif isinstance(status, ReleaseCandidateStatus) or isinstance(status, CandidateStatus):\n kwargs[\"status\"] = status.value\n if not str(kwargs.get(\"id\", \"\")).strip():\n raise ValueError(\"candidate_id must be non-empty\")\n super().__init__(**kwargs)\n\n @property\n def candidate_id(self) -> str:\n return self.id\n\n def transition_to(self, new_status: CandidateStatus):\n \"\"\"\n @PURPOSE: Enforce legal state transitions.\n @PRE: Transition must be allowed by lifecycle rules.\n \"\"\"\n allowed = {\n CandidateStatus.DRAFT: [CandidateStatus.PREPARED],\n CandidateStatus.PREPARED: [CandidateStatus.MANIFEST_BUILT],\n CandidateStatus.MANIFEST_BUILT: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_PENDING: [CandidateStatus.CHECK_RUNNING],\n CandidateStatus.CHECK_RUNNING: [\n CandidateStatus.CHECK_PASSED,\n CandidateStatus.CHECK_BLOCKED,\n CandidateStatus.CHECK_ERROR\n ],\n CandidateStatus.CHECK_PASSED: [CandidateStatus.APPROVED, CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_BLOCKED: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.CHECK_ERROR: [CandidateStatus.CHECK_PENDING],\n CandidateStatus.APPROVED: [CandidateStatus.PUBLISHED],\n CandidateStatus.PUBLISHED: [CandidateStatus.REVOKED],\n CandidateStatus.REVOKED: []\n }\n current_status = CandidateStatus(self.status)\n if new_status not in allowed.get(current_status, []):\n raise IllegalTransitionError(f\"Forbidden transition from {current_status} to {new_status}\")\n self.status = new_status.value\n# #endregion ReleaseCandidate\n" }, { "contract_id": "CandidateArtifact", @@ -32239,7 +33112,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n" + "body": "# #region DistributionManifest [TYPE Class]\n# @BRIEF Immutable snapshot of the candidate payload.\n# @INVARIANT Immutable after creation.\nclass DistributionManifest(Base):\n __tablename__ = \"clean_release_manifests\"\n\n id = Column(String, primary_key=True)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n manifest_version = Column(Integer, nullable=False)\n manifest_digest = Column(String, nullable=False)\n artifacts_digest = Column(String, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n created_by = Column(String, nullable=False)\n source_snapshot_ref = Column(String, nullable=False)\n content_json = Column(JSON, nullable=False)\n immutable = Column(Boolean, default=True)\n\n # Redesign compatibility fields (not persisted directly but used by builder/facade)\n def __init__(self, **kwargs):\n items = kwargs.pop(\"items\", None)\n summary = kwargs.pop(\"summary\", None)\n\n # Handle fields from manifest_builder.py\n if \"manifest_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"manifest_id\")\n if \"generated_at\" in kwargs:\n kwargs[\"created_at\"] = kwargs.pop(\"generated_at\")\n if \"generated_by\" in kwargs:\n kwargs[\"created_by\"] = kwargs.pop(\"generated_by\")\n if \"deterministic_hash\" in kwargs:\n kwargs[\"manifest_digest\"] = kwargs.pop(\"deterministic_hash\")\n if \"policy_id\" in kwargs:\n kwargs.pop(\"policy_id\")\n\n if items is not None and summary is not None:\n expected_count = int(summary.included_count) + int(summary.excluded_count)\n if expected_count != len(items):\n raise ValueError(\"manifest summary counts must match items size\")\n\n # Ensure required DB fields have defaults if missing\n if \"manifest_version\" not in kwargs:\n kwargs[\"manifest_version\"] = 1\n if \"artifacts_digest\" not in kwargs:\n kwargs[\"artifacts_digest\"] = kwargs.get(\"manifest_digest\", \"pending\")\n if \"source_snapshot_ref\" not in kwargs:\n kwargs[\"source_snapshot_ref\"] = \"pending\"\n\n # Pack items and summary into content_json if provided\n if items is not None or summary is not None:\n content = dict(kwargs.get(\"content_json\") or {})\n if items is not None:\n content[\"items\"] = [\n {\n \"path\": i.path,\n \"category\": i.category,\n \"classification\": i.classification.value,\n \"reason\": i.reason,\n \"checksum\": i.checksum\n } for i in items\n ]\n if summary is not None:\n content[\"summary\"] = {\n \"included_count\": summary.included_count,\n \"excluded_count\": summary.excluded_count,\n \"prohibited_detected_count\": summary.prohibited_detected_count\n }\n kwargs[\"content_json\"] = content\n\n super().__init__(**kwargs)\n\n @property\n def manifest_id(self) -> str:\n return self.id\n\n @property\n def deterministic_hash(self) -> str:\n return self.manifest_digest\n\n @property\n def summary(self) -> ManifestSummary:\n payload = (self.content_json or {}).get(\"summary\", {})\n return ManifestSummary(\n included_count=int(payload.get(\"included_count\", 0)),\n excluded_count=int(payload.get(\"excluded_count\", 0)),\n prohibited_detected_count=int(payload.get(\"prohibited_detected_count\", 0)),\n )\n# #endregion DistributionManifest\n" }, { "contract_id": "SourceRegistrySnapshot", @@ -32256,7 +33129,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF 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# #endregion SourceRegistrySnapshot\n" + "body": "# #region SourceRegistrySnapshot [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion SourceRegistrySnapshot\n" }, { "contract_id": "CleanPolicySnapshot", @@ -32273,7 +33146,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF 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# #endregion CleanPolicySnapshot\n" + "body": "# #region CleanPolicySnapshot [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion CleanPolicySnapshot\n" }, { "contract_id": "ComplianceRun", @@ -32290,7 +33163,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ComplianceRun [TYPE Class]\n# @BRIEF 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# #endregion ComplianceRun\n" + "body": "# #region ComplianceRun [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\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# #endregion ComplianceRun\n" }, { "contract_id": "ComplianceStageRun", @@ -32376,7 +33249,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=datetime.utcnow)\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n" + "body": "# #region ComplianceReport [TYPE Class]\n# @BRIEF Immutable result derived from a completed run.\n# @INVARIANT Immutable after creation.\nclass ComplianceReport(Base):\n __tablename__ = \"clean_release_compliance_reports\"\n\n id = Column(String, primary_key=True)\n run_id = Column(String, ForeignKey(\"clean_release_compliance_runs.id\"), nullable=False)\n candidate_id = Column(String, ForeignKey(\"clean_release_candidates.id\"), nullable=False)\n final_status = Column(String, nullable=False) # ComplianceDecision\n summary_json = Column(JSON, nullable=False)\n generated_at = Column(DateTime, default=lambda: datetime.now(UTC))\n immutable = Column(Boolean, default=True)\n\n def __init__(self, **kwargs):\n if \"report_id\" in kwargs:\n kwargs[\"id\"] = kwargs.pop(\"report_id\")\n if \"check_run_id\" in kwargs:\n kwargs[\"run_id\"] = kwargs.pop(\"check_run_id\")\n operator_summary = kwargs.pop(\"operator_summary\", None)\n structured_payload_ref = kwargs.pop(\"structured_payload_ref\", None)\n violations_count = kwargs.pop(\"violations_count\", None)\n blocking_violations_count = kwargs.pop(\"blocking_violations_count\", None)\n\n final_status = kwargs.get(\"final_status\")\n final_status_value = getattr(final_status, \"value\", final_status)\n\n if (\n final_status_value in {CheckFinalStatus.BLOCKED.value, ComplianceDecision.BLOCKED.value}\n and blocking_violations_count is not None\n and int(blocking_violations_count) <= 0\n ):\n raise ValueError(\"blocked report requires blocking violations\")\n\n if (\n operator_summary is not None\n or structured_payload_ref is not None\n or violations_count is not None\n or blocking_violations_count is not None\n ):\n kwargs[\"summary_json\"] = {\n \"operator_summary\": operator_summary or \"\",\n \"structured_payload_ref\": structured_payload_ref,\n \"violations_count\": int(violations_count or 0),\n \"blocking_violations_count\": int(blocking_violations_count or 0),\n }\n\n super().__init__(**kwargs)\n\n @property\n def report_id(self) -> str:\n return self.id\n\n @property\n def check_run_id(self) -> str:\n return self.run_id\n\n @property\n def operator_summary(self) -> str:\n return (self.summary_json or {}).get(\"operator_summary\", \"\")\n\n @property\n def structured_payload_ref(self) -> str | None:\n return (self.summary_json or {}).get(\"structured_payload_ref\")\n\n @property\n def violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"violations_count\", 0))\n\n @property\n def blocking_violations_count(self) -> int:\n return int((self.summary_json or {}).get(\"blocking_violations_count\", 0))\n# #endregion ComplianceReport\n" }, { "contract_id": "ApprovalDecision", @@ -32393,7 +33266,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ApprovalDecision [TYPE Class]\n# @BRIEF 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# #endregion ApprovalDecision\n" + "body": "# #region ApprovalDecision [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n comment = Column(String, nullable=True)\n# #endregion ApprovalDecision\n" }, { "contract_id": "PublicationRecord", @@ -32410,7 +33283,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PublicationRecord [TYPE Class]\n# @BRIEF 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# #endregion PublicationRecord\n" + "body": "# #region PublicationRecord [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n target_channel = Column(String, nullable=False)\n publication_ref = Column(String, nullable=True)\n status = Column(String, default=PublicationStatus.ACTIVE)\n# #endregion PublicationRecord\n" }, { "contract_id": "CleanReleaseAuditLog", @@ -32427,7 +33300,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\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# #endregion CleanReleaseAuditLog\n" + "body": "# #region CleanReleaseAuditLog [TYPE Class]\n# @BRIEF Represents a persistent audit log entry for clean release actions.\nimport uuid\n\n\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=lambda: datetime.now(UTC))\n details_json = Column(JSON, default=dict)\n# #endregion CleanReleaseAuditLog\n" }, { "contract_id": "AppConfigRecord", @@ -32645,7 +33518,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewClarificationModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, clarification, model]\n# @BRIEF Clarification session, question, option, and answer models for guided review flow.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Only one active clarification question may exist at a time per session.\n# @PRE Database engine initialized\n# @POST Clarification ORM models registered\n# @SIDE_EFFECT Defines dataset review clarification tables\n# @DATA_CONTRACT ClarificationData -> ClarificationRecord\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n AnswerKind,\n ClarificationStatus,\n QuestionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n\n\n# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n\n\n# #region ClarificationOption [C:1] [TYPE Class]\n# @BRIEF One selectable option for a clarification question with recommendation flag.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationOption(Base):\n __tablename__ = \"clarification_options\"\n\n option_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False)\n label = Column(String, nullable=False)\n value = Column(String, nullable=False)\n is_recommended = Column(Boolean, nullable=False, default=False)\n display_order = Column(Integer, nullable=False, default=0)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"options\")\n\n\n# #endregion ClarificationOption\n\n\n# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n\n\n# #endregion DatasetReviewClarificationModels\n" + "body": "# #region DatasetReviewClarificationModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, clarification, model]\n# @BRIEF Clarification session, question, option, and answer models for guided review flow.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Only one active clarification question may exist at a time per session.\n# @PRE Database engine initialized\n# @POST Clarification ORM models registered\n# @SIDE_EFFECT Defines dataset review clarification tables\n# @DATA_CONTRACT ClarificationData -> ClarificationRecord\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n AnswerKind,\n ClarificationStatus,\n QuestionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n\n\n# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n\n\n# #region ClarificationOption [C:1] [TYPE Class]\n# @BRIEF One selectable option for a clarification question with recommendation flag.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationOption(Base):\n __tablename__ = \"clarification_options\"\n\n option_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False)\n label = Column(String, nullable=False)\n value = Column(String, nullable=False)\n is_recommended = Column(Boolean, nullable=False, default=False)\n display_order = Column(Integer, nullable=False, default=0)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"options\")\n\n\n# #endregion ClarificationOption\n\n\n# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n\n\n# #endregion DatasetReviewClarificationModels\n" }, { "contract_id": "ClarificationSession", @@ -32670,7 +33543,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n" + "body": "# #region ClarificationSession [C:2] [TYPE Class]\n# @BRIEF One clarification session aggregate owning questions and tracking resolution progress.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ClarificationSession(Base):\n __tablename__ = \"clarification_sessions\"\n\n clarification_session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n status = Column(SQLEnum(ClarificationStatus), nullable=False, default=ClarificationStatus.PENDING)\n current_question_id = Column(String, nullable=True)\n resolved_count = Column(Integer, nullable=False, default=0)\n remaining_count = Column(Integer, nullable=False, default=0)\n summary_delta = Column(Text, nullable=True)\n started_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n completed_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"clarification_sessions\")\n questions = relationship(\"ClarificationQuestion\", back_populates=\"clarification_session\", cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationSession\n" }, { "contract_id": "ClarificationQuestion", @@ -32695,7 +33568,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n" + "body": "# #region ClarificationQuestion [C:2] [TYPE Class]\n# @BRIEF One clarification question with priority ordering, options, and state machine.\n# @RELATION DEPENDS_ON -> [ClarificationSession]\nclass ClarificationQuestion(Base):\n __tablename__ = \"clarification_questions\"\n\n question_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n clarification_session_id = Column(String, ForeignKey(\"clarification_sessions.clarification_session_id\"), nullable=False)\n topic_ref = Column(String, nullable=False)\n question_text = Column(Text, nullable=False)\n why_it_matters = Column(Text, nullable=False)\n current_guess = Column(Text, nullable=True)\n priority = Column(Integer, nullable=False, default=0)\n state = Column(SQLEnum(QuestionState), nullable=False, default=QuestionState.OPEN)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n\n clarification_session = relationship(\"ClarificationSession\", back_populates=\"questions\")\n options = relationship(\"ClarificationOption\", back_populates=\"question\", cascade=\"all, delete-orphan\")\n answer = relationship(\"ClarificationAnswer\", back_populates=\"question\", uselist=False, cascade=\"all, delete-orphan\")\n\n\n# #endregion ClarificationQuestion\n" }, { "contract_id": "ClarificationOption", @@ -32745,7 +33618,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n" + "body": "# #region ClarificationAnswer [C:2] [TYPE Class]\n# @BRIEF One persisted clarification answer with impact summary and feedback tracking.\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\nclass ClarificationAnswer(Base):\n __tablename__ = \"clarification_answers\"\n\n answer_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n question_id = Column(String, ForeignKey(\"clarification_questions.question_id\"), nullable=False, unique=True)\n answer_kind = Column(SQLEnum(AnswerKind), nullable=False)\n answer_value = Column(Text, nullable=True)\n answered_by_user_id = Column(String, nullable=False)\n impact_summary = Column(Text, nullable=True)\n user_feedback = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n question = relationship(\"ClarificationQuestion\", back_populates=\"answer\")\n\n\n# #endregion ClarificationAnswer\n" }, { "contract_id": "DatasetReviewEnums", @@ -33373,7 +34246,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewExecutionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, execution, model]\n# @BRIEF Compiled preview, run context, session event, and export artifact models for execution and audit.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ArtifactFormat,\n ArtifactType,\n LaunchStatus,\n PreviewStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n\n\n# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n\n\n# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n\n\n# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n\n\n# #endregion DatasetReviewExecutionModels\n" + "body": "# #region DatasetReviewExecutionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, execution, model]\n# @BRIEF Compiled preview, run context, session event, and export artifact models for execution and audit.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ArtifactFormat,\n ArtifactType,\n LaunchStatus,\n PreviewStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n\n\n# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n\n\n# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n\n\n# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n\n\n# #endregion DatasetReviewExecutionModels\n" }, { "contract_id": "CompiledPreview", @@ -33398,7 +34271,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n" + "body": "# #region CompiledPreview [C:2] [TYPE Class]\n# @BRIEF One compiled SQL preview snapshot with fingerprint for staleness detection.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass CompiledPreview(Base):\n __tablename__ = \"compiled_previews\"\n\n preview_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n preview_status = Column(\n SQLEnum(PreviewStatus), nullable=False, default=PreviewStatus.PENDING\n )\n compiled_sql = Column(Text, nullable=True)\n preview_fingerprint = Column(String, nullable=False)\n compiled_by = Column(String, nullable=False, default=\"superset\")\n error_code = Column(String, nullable=True)\n error_details = Column(Text, nullable=True)\n compiled_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"previews\")\n\n\n# #endregion CompiledPreview\n" }, { "contract_id": "DatasetRunContext", @@ -33423,7 +34296,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n" + "body": "# #region DatasetRunContext [C:2] [TYPE Class]\n# @BRIEF Immutable launch audit snapshot capturing effective filters, template params, and approval state at launch time.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetRunContext(Base):\n __tablename__ = \"dataset_run_contexts\"\n\n run_context_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n dataset_ref = Column(String, nullable=False)\n environment_id = Column(String, nullable=False)\n preview_id = Column(String, nullable=False)\n sql_lab_session_ref = Column(String, nullable=False)\n effective_filters = Column(JSON, nullable=False)\n template_params = Column(JSON, nullable=False)\n approved_mapping_ids = Column(JSON, nullable=False)\n semantic_decision_refs = Column(JSON, nullable=False)\n open_warning_refs = Column(JSON, nullable=False)\n launch_status = Column(SQLEnum(LaunchStatus), nullable=False)\n launch_error = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"run_contexts\")\n\n\n# #endregion DatasetRunContext\n" }, { "contract_id": "SessionEvent", @@ -33448,7 +34321,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n" + "body": "# #region SessionEvent [C:2] [TYPE Class]\n# @BRIEF One persisted audit event for dataset review session mutations.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionEvent(Base):\n __tablename__ = \"session_events\"\n\n session_event_id = Column(\n String, primary_key=True, default=lambda: str(uuid.uuid4())\n )\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n actor_user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n event_type = Column(String, nullable=False)\n event_summary = Column(Text, nullable=False)\n current_phase = Column(String, nullable=True)\n readiness_state = Column(String, nullable=True)\n event_details = Column(JSON, nullable=False, default=dict)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"events\")\n actor = relationship(\"User\")\n\n\n# #endregion SessionEvent\n" }, { "contract_id": "ExportArtifact", @@ -33473,7 +34346,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n" + "body": "# #region ExportArtifact [C:2] [TYPE Class]\n# @BRIEF One persisted export artifact reference for documentation and validation outputs.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ExportArtifact(Base):\n __tablename__ = \"export_artifacts\"\n\n artifact_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n artifact_type = Column(SQLEnum(ArtifactType), nullable=False)\n format = Column(SQLEnum(ArtifactFormat), nullable=False)\n storage_ref = Column(String, nullable=False)\n created_by_user_id = Column(String, nullable=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"export_artifacts\")\n\n\n# #endregion ExportArtifact\n" }, { "contract_id": "DatasetReviewFilterModels", @@ -33505,7 +34378,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewFilterModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, filter, template]\n# @BRIEF Imported filter and template variable models for Superset context recovery and execution mapping.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n MappingStatus,\n VariableKind,\n)\nfrom src.models.mapping import Base\n\n\n# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n\n\n# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n\n\n# #endregion DatasetReviewFilterModels\n" + "body": "# #region DatasetReviewFilterModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, filter, template]\n# @BRIEF Imported filter and template variable models for Superset context recovery and execution mapping.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n MappingStatus,\n VariableKind,\n)\nfrom src.models.mapping import Base\n\n\n# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n\n\n# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n\n\n# #endregion DatasetReviewFilterModels\n" }, { "contract_id": "ImportedFilter", @@ -33530,7 +34403,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n" + "body": "# #region ImportedFilter [C:2] [TYPE Class]\n# @BRIEF Recovered Superset filter with confidence and recovery status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ImportedFilter(Base):\n __tablename__ = \"imported_filters\"\n\n filter_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n filter_name = Column(String, nullable=False)\n display_name = Column(String, nullable=True)\n raw_value = Column(JSON, nullable=False)\n raw_value_masked = Column(Boolean, nullable=False, default=False)\n normalized_value = Column(JSON, nullable=True)\n source = Column(SQLEnum(FilterSource), nullable=False)\n confidence_state = Column(SQLEnum(FilterConfidenceState), nullable=False)\n requires_confirmation = Column(Boolean, nullable=False, default=False)\n recovery_status = Column(SQLEnum(FilterRecoveryStatus), nullable=False)\n notes = Column(Text, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"imported_filters\")\n\n\n# #endregion ImportedFilter\n" }, { "contract_id": "TemplateVariable", @@ -33555,7 +34428,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n" + "body": "# #region TemplateVariable [C:2] [TYPE Class]\n# @BRIEF Discovered template variable from dataset SQL with mapping status tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass TemplateVariable(Base):\n __tablename__ = \"template_variables\"\n\n variable_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n variable_name = Column(String, nullable=False)\n expression_source = Column(Text, nullable=False)\n variable_kind = Column(SQLEnum(VariableKind), nullable=False)\n is_required = Column(Boolean, nullable=False, default=True)\n default_value = Column(JSON, nullable=True)\n mapping_status = Column(\n SQLEnum(MappingStatus), nullable=False, default=MappingStatus.UNMAPPED\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"template_variables\")\n\n\n# #endregion TemplateVariable\n" }, { "contract_id": "DatasetReviewFindingModels", @@ -33587,7 +34460,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewFindingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, finding, validation]\n# @BRIEF Validation finding model for tracking blocking, warning, and informational issues during review.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n FindingArea,\n FindingSeverity,\n ResolutionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n\n\n# #endregion DatasetReviewFindingModels\n" + "body": "# #region DatasetReviewFindingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, finding, validation]\n# @BRIEF Validation finding model for tracking blocking, warning, and informational issues during review.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n FindingArea,\n FindingSeverity,\n ResolutionState,\n)\nfrom src.models.mapping import Base\n\n\n# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n\n\n# #endregion DatasetReviewFindingModels\n" }, { "contract_id": "ValidationFinding", @@ -33612,7 +34485,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n" + "body": "# #region ValidationFinding [C:2] [TYPE Class]\n# @BRIEF Structured finding record for dataset review validation issues with resolution tracking.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass ValidationFinding(Base):\n __tablename__ = \"validation_findings\"\n\n finding_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n area = Column(SQLEnum(FindingArea), nullable=False)\n severity = Column(SQLEnum(FindingSeverity), nullable=False)\n code = Column(String, nullable=False)\n title = Column(String, nullable=False)\n message = Column(Text, nullable=False)\n resolution_state = Column(\n SQLEnum(ResolutionState), nullable=False, default=ResolutionState.OPEN\n )\n resolution_note = Column(Text, nullable=True)\n caused_by_ref = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n resolved_at = Column(DateTime, nullable=True)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"findings\")\n\n\n# #endregion ValidationFinding\n" }, { "contract_id": "DatasetReviewMappingModels", @@ -33644,7 +34517,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewMappingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, mapping, model]\n# @BRIEF Execution mapping model linking imported filters to template variables with approval gates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ApprovalState,\n MappingMethod,\n MappingWarningLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n\n\n# #endregion DatasetReviewMappingModels\n" + "body": "# #region DatasetReviewMappingModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, mapping, model]\n# @BRIEF Execution mapping model linking imported filters to template variables with approval gates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ApprovalState,\n MappingMethod,\n MappingWarningLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n\n\n# #endregion DatasetReviewMappingModels\n" }, { "contract_id": "ExecutionMapping", @@ -33670,7 +34543,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n" + "body": "# #region ExecutionMapping [C:2] [TYPE Class]\n# @BRIEF One filter-to-variable mapping with approval gate, effective value, and transformation metadata.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @INVARIANT Explicit approval is required before launch when requires_explicit_approval is true.\nclass ExecutionMapping(Base):\n __tablename__ = \"execution_mappings\"\n\n mapping_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False)\n filter_id = Column(String, nullable=False)\n variable_id = Column(String, nullable=False)\n mapping_method = Column(SQLEnum(MappingMethod), nullable=False)\n raw_input_value = Column(JSON, nullable=False)\n effective_value = Column(JSON, nullable=True)\n transformation_note = Column(Text, nullable=True)\n warning_level = Column(SQLEnum(MappingWarningLevel), nullable=True)\n requires_explicit_approval = Column(Boolean, nullable=False, default=False)\n approval_state = Column(SQLEnum(ApprovalState), nullable=False, default=ApprovalState.NOT_REQUIRED)\n approved_by_user_id = Column(String, nullable=True)\n approved_at = Column(DateTime, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"execution_mappings\")\n\n\n# #endregion ExecutionMapping\n" }, { "contract_id": "DatasetReviewProfileModels", @@ -33702,7 +34575,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewProfileModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, profile, summary]\n# @BRIEF Dataset profile model capturing business summary, confidence, and completeness metadata.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n Float,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n BusinessSummarySource,\n ConfidenceState,\n)\nfrom src.models.mapping import Base\n\n\n# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n\n\n# #endregion DatasetReviewProfileModels\n" + "body": "# #region DatasetReviewProfileModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, profile, summary]\n# @BRIEF Dataset profile model capturing business summary, confidence, and completeness metadata.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n Float,\n ForeignKey,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n BusinessSummarySource,\n ConfidenceState,\n)\nfrom src.models.mapping import Base\n\n\n# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n\n\n# #endregion DatasetReviewProfileModels\n" }, { "contract_id": "DatasetProfile", @@ -33727,7 +34600,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n" + "body": "# #region DatasetProfile [C:2] [TYPE Class]\n# @BRIEF One-to-one profile snapshot for a dataset review session, tracking business summary provenance and completeness.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass DatasetProfile(Base):\n __tablename__ = \"dataset_profiles\"\n\n profile_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String,\n ForeignKey(\"dataset_review_sessions.session_id\"),\n nullable=False,\n unique=True,\n )\n dataset_name = Column(String, nullable=False)\n schema_name = Column(String, nullable=True)\n database_name = Column(String, nullable=True)\n business_summary = Column(Text, nullable=False)\n business_summary_source = Column(SQLEnum(BusinessSummarySource), nullable=False)\n description = Column(Text, nullable=True)\n dataset_type = Column(String, nullable=True)\n is_sqllab_view = Column(Boolean, nullable=False, default=False)\n completeness_score = Column(Float, nullable=True)\n confidence_state = Column(SQLEnum(ConfidenceState), nullable=False)\n has_blocking_findings = Column(Boolean, nullable=False, default=False)\n has_warning_findings = Column(Boolean, nullable=False, default=False)\n manual_summary_locked = Column(Boolean, nullable=False, default=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"profile\")\n\n\n# #endregion DatasetProfile\n" }, { "contract_id": "DatasetReviewSemanticModels", @@ -33764,7 +34637,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSemanticModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, semantic, enrichment]\n# @BRIEF Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n# @PRE Database engine initialized\n# @POST Semantic ORM models registered with override protection\n# @SIDE_EFFECT Defines dataset review semantic tables\n# @DATA_CONTRACT SemanticData -> SemanticFieldEntry\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n Float,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n CandidateMatchType,\n CandidateStatus,\n FieldKind,\n FieldProvenance,\n SemanticSourceStatus,\n SemanticSourceType,\n TrustLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n\n\n# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n\n\n# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n\n\n# #endregion DatasetReviewSemanticModels\n" + "body": "# #region DatasetReviewSemanticModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, semantic, enrichment]\n# @BRIEF Semantic source, field entry, and candidate models for dictionary-driven semantic enrichment.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Manual overrides are never silently replaced by imported, inferred, or AI-generated values.\n# @PRE Database engine initialized\n# @POST Semantic ORM models registered with override protection\n# @SIDE_EFFECT Defines dataset review semantic tables\n# @DATA_CONTRACT SemanticData -> SemanticFieldEntry\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n Boolean,\n Column,\n DateTime,\n Enum as SQLEnum,\n Float,\n ForeignKey,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n CandidateMatchType,\n CandidateStatus,\n FieldKind,\n FieldProvenance,\n SemanticSourceStatus,\n SemanticSourceType,\n TrustLevel,\n)\nfrom src.models.mapping import Base\n\n\n# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n\n\n# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n\n\n# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n\n\n# #endregion DatasetReviewSemanticModels\n" }, { "contract_id": "SemanticSource", @@ -33789,7 +34662,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n" + "body": "# #region SemanticSource [C:2] [TYPE Class]\n# @BRIEF Registered semantic enrichment source with trust level and application status.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SemanticSource(Base):\n __tablename__ = \"semantic_sources\"\n\n source_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n source_type = Column(SQLEnum(SemanticSourceType), nullable=False)\n source_ref = Column(String, nullable=False)\n source_version = Column(String, nullable=False)\n display_name = Column(String, nullable=False)\n trust_level = Column(SQLEnum(TrustLevel), nullable=False)\n schema_overlap_score = Column(Float, nullable=True)\n status = Column(\n SQLEnum(SemanticSourceStatus),\n nullable=False,\n default=SemanticSourceStatus.AVAILABLE,\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_sources\")\n\n\n# #endregion SemanticSource\n" }, { "contract_id": "SemanticFieldEntry", @@ -33821,7 +34694,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n" + "body": "# #region SemanticFieldEntry [C:3] [TYPE Class]\n# @BRIEF Per-field semantic metadata entry with provenance tracking, lock state, and candidate set.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SemanticCandidate]\n# @INVARIANT Locked fields preserve their active value regardless of later candidate proposals.\nclass SemanticFieldEntry(Base):\n __tablename__ = \"semantic_field_entries\"\n\n field_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n field_name = Column(String, nullable=False)\n field_kind = Column(SQLEnum(FieldKind), nullable=False)\n verbose_name = Column(String, nullable=True)\n description = Column(Text, nullable=True)\n display_format = Column(String, nullable=True)\n provenance = Column(\n SQLEnum(FieldProvenance), nullable=False, default=FieldProvenance.UNRESOLVED\n )\n source_id = Column(String, nullable=True)\n source_version = Column(String, nullable=True)\n confidence_rank = Column(Integer, nullable=True)\n is_locked = Column(Boolean, nullable=False, default=False)\n has_conflict = Column(Boolean, nullable=False, default=False)\n needs_review = Column(Boolean, nullable=False, default=True)\n last_changed_by = Column(String, nullable=False)\n user_feedback = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"semantic_fields\")\n candidates = relationship(\n \"SemanticCandidate\", back_populates=\"field\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion SemanticFieldEntry\n" }, { "contract_id": "SemanticCandidate", @@ -33846,7 +34719,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n" + "body": "# #region SemanticCandidate [C:2] [TYPE Class]\n# @BRIEF One proposed semantic value for a field entry, ranked by match type and confidence.\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\nclass SemanticCandidate(Base):\n __tablename__ = \"semantic_candidates\"\n\n candidate_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n field_id = Column(\n String, ForeignKey(\"semantic_field_entries.field_id\"), nullable=False\n )\n source_id = Column(String, nullable=True)\n candidate_rank = Column(Integer, nullable=False)\n match_type = Column(SQLEnum(CandidateMatchType), nullable=False)\n confidence_score = Column(Float, nullable=False)\n proposed_verbose_name = Column(String, nullable=True)\n proposed_description = Column(Text, nullable=True)\n proposed_display_format = Column(String, nullable=True)\n status = Column(\n SQLEnum(CandidateStatus), nullable=False, default=CandidateStatus.PROPOSED\n )\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n field = relationship(\"SemanticFieldEntry\", back_populates=\"candidates\")\n\n\n# #endregion SemanticCandidate\n" }, { "contract_id": "DatasetReviewSessionModels", @@ -33883,7 +34756,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSessionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, session, model]\n# @BRIEF Session aggregate root and collaborator models for dataset review orchestration.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Session and profile entities are strictly scoped to an authenticated user.\n# @PRE Database engine initialized\n# @POST Session ORM models registered with optimistic locking\n# @SIDE_EFFECT Defines dataset review session tables\n# @DATA_CONTRACT SessionData -> SessionRecord\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Integer,\n String,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n SessionPhase,\n SessionStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n\n\n# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n last_activity_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n\n\n# #endregion DatasetReviewSessionModels\n" + "body": "# #region DatasetReviewSessionModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, dataset, review, session, model]\n# @BRIEF Session aggregate root and collaborator models for dataset review orchestration.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewEnums]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT Session and profile entities are strictly scoped to an authenticated user.\n# @PRE Database engine initialized\n# @POST Session ORM models registered with optimistic locking\n# @SIDE_EFFECT Defines dataset review session tables\n# @DATA_CONTRACT SessionData -> SessionRecord\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import (\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Integer,\n String,\n)\nfrom sqlalchemy.orm import relationship\n\nfrom src.models.dataset_review_pkg._enums import (\n ReadinessState,\n RecommendedAction,\n SessionCollaboratorRole,\n SessionPhase,\n SessionStatus,\n)\nfrom src.models.mapping import Base\n\n\n# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n\n\n# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n last_activity_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n\n\n# #endregion DatasetReviewSessionModels\n" }, { "contract_id": "SessionCollaborator", @@ -33908,7 +34781,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n" + "body": "# #region SessionCollaborator [C:2] [TYPE Class]\n# @BRIEF RBAC collaborator record linking a user to a dataset review session with a specific role.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\nclass SessionCollaborator(Base):\n __tablename__ = \"session_collaborators\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n session_id = Column(\n String, ForeignKey(\"dataset_review_sessions.session_id\"), nullable=False\n )\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n role = Column(SQLEnum(SessionCollaboratorRole), nullable=False)\n added_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n\n session = relationship(\"DatasetReviewSession\", back_populates=\"collaborators\")\n user = relationship(\"User\")\n\n\n# #endregion SessionCollaborator\n" }, { "contract_id": "DatasetReviewSession", @@ -34006,7 +34879,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n updated_at = Column(\n DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False\n )\n last_activity_at = Column(DateTime, default=datetime.utcnow, nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n" + "body": "# #region DatasetReviewSession [C:3] [TYPE Class]\n# @BRIEF Aggregate root for the dataset review lifecycle, owning all child entities and driving readiness transitions.\n# @RELATION DEPENDS_ON -> [SessionCollaborator]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [SemanticSource]\n# @RELATION DEPENDS_ON -> [SemanticFieldEntry]\n# @RELATION DEPENDS_ON -> [ImportedFilter]\n# @RELATION DEPENDS_ON -> [TemplateVariable]\n# @RELATION DEPENDS_ON -> [ExecutionMapping]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DEPENDS_ON -> [DatasetRunContext]\n# @RELATION DEPENDS_ON -> [ExportArtifact]\n# @RELATION DEPENDS_ON -> [SessionEvent]\n# @INVARIANT Optimistic-lock version column prevents lost-update races on concurrent mutations.\nclass DatasetReviewSession(Base):\n __tablename__ = \"dataset_review_sessions\"\n\n session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False)\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_kind = Column(String, nullable=False)\n source_input = Column(String, nullable=False)\n dataset_ref = Column(String, nullable=False)\n dataset_id = Column(Integer, nullable=True)\n dashboard_id = Column(Integer, nullable=True)\n readiness_state = Column(\n SQLEnum(ReadinessState), nullable=False, default=ReadinessState.EMPTY\n )\n recommended_action = Column(\n SQLEnum(RecommendedAction),\n nullable=False,\n default=RecommendedAction.IMPORT_FROM_SUPERSET,\n )\n version = Column(Integer, nullable=False, default=0)\n __mapper_args__ = {\"version_id_col\": version, \"version_id_generator\": False}\n status = Column(\n SQLEnum(SessionStatus), nullable=False, default=SessionStatus.ACTIVE\n )\n current_phase = Column(\n SQLEnum(SessionPhase), nullable=False, default=SessionPhase.INTAKE\n )\n active_task_id = Column(String, nullable=True)\n last_preview_id = Column(String, nullable=True)\n last_run_context_id = Column(String, nullable=True)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n updated_at = Column(\n DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC), nullable=False\n )\n last_activity_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)\n closed_at = Column(DateTime, nullable=True)\n\n owner = relationship(\"User\")\n collaborators = relationship(\n \"SessionCollaborator\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n profile = relationship(\n \"DatasetProfile\",\n back_populates=\"session\",\n uselist=False,\n cascade=\"all, delete-orphan\",\n )\n findings = relationship(\n \"ValidationFinding\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_sources = relationship(\n \"SemanticSource\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n semantic_fields = relationship(\n \"SemanticFieldEntry\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n imported_filters = relationship(\n \"ImportedFilter\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n template_variables = relationship(\n \"TemplateVariable\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n execution_mappings = relationship(\n \"ExecutionMapping\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n clarification_sessions = relationship(\n \"ClarificationSession\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n previews = relationship(\n \"CompiledPreview\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n run_contexts = relationship(\n \"DatasetRunContext\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n export_artifacts = relationship(\n \"ExportArtifact\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n events = relationship(\n \"SessionEvent\", back_populates=\"session\", cascade=\"all, delete-orphan\"\n )\n\n\n# #endregion DatasetReviewSession\n" }, { "contract_id": "FilterStateModels", @@ -34142,7 +35015,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitModels [TYPE Module] [SEMANTICS sqlalchemy, git, model, config, sync]\n\nfrom datetime import datetime\nimport enum\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, String\n\nfrom src.core.database import Base\n\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# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF 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# #endregion GitServerConfig\n\n# #region GitRepository [C:1] [TYPE Class]\n# @BRIEF 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# #endregion GitRepository\n\n# #region DeploymentEnvironment [C:1] [TYPE Class]\n# @BRIEF 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# #endregion DeploymentEnvironment\n\n# #endregion GitModels\n" + "body": "# #region GitModels [TYPE Module] [SEMANTICS sqlalchemy, git, model, config, sync]\n\nfrom datetime import UTC, datetime\nimport enum\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, String\n\nfrom src.core.database import Base\n\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# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n# #endregion GitServerConfig\n\n# #region GitRepository [C:1] [TYPE Class]\n# @BRIEF 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# #endregion GitRepository\n\n# #region DeploymentEnvironment [C:1] [TYPE Class]\n# @BRIEF 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# #endregion DeploymentEnvironment\n\n# #endregion GitModels\n" }, { "contract_id": "GitServerConfig", @@ -34160,7 +35033,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF 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# #endregion GitServerConfig\n" + "body": "# #region GitServerConfig [C:1] [TYPE Class]\n# @BRIEF 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=lambda: datetime.now(UTC))\n# #endregion GitServerConfig\n" }, { "contract_id": "GitRepository", @@ -34222,7 +35095,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LlmModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, model, schema, validate, provider]\n# @BRIEF SQLAlchemy models for LLM provider configuration and validation results.\n# @LAYER Domain\n# @RELATION INHERITS -> [MappingModels]\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, String, Text, Time\n\nfrom .mapping import Base\n\n\ndef generate_uuid():\n return str(uuid.uuid4())\n\n# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\n# @RELATION DEPENDS_ON -> [LLMProvider]\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 provider_id = Column(String, nullable=True) # Reference to LLMProvider.id\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# #endregion ValidationPolicy\n\n# #region LLMProvider [TYPE Class]\n# @BRIEF 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 is_multimodal = Column(Boolean, default=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n# #endregion LLMProvider\n\n# #region ValidationRecord [TYPE Class]\n# @BRIEF 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 policy_id = Column(String, nullable=True, index=True) # Reference to ValidationPolicy.id\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# #endregion ValidationRecord\n\n# #endregion LlmModels\n" + "body": "# #region LlmModels [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, model, schema, validate, provider]\n# @BRIEF SQLAlchemy models for LLM provider configuration and validation results.\n# @LAYER Domain\n# @RELATION INHERITS -> [MappingModels]\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import JSON, Boolean, Column, DateTime, String, Text, Time\n\nfrom .mapping import Base\n\n\ndef generate_uuid():\n return str(uuid.uuid4())\n\n# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\n# @RELATION DEPENDS_ON -> [LLMProvider]\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 provider_id = Column(String, nullable=True) # Reference to LLMProvider.id\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=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion ValidationPolicy\n\n# #region LLMProvider [TYPE Class]\n# @BRIEF 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 is_multimodal = Column(Boolean, default=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion LLMProvider\n\n# #region ValidationRecord [TYPE Class]\n# @BRIEF 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 policy_id = Column(String, nullable=True, index=True) # Reference to ValidationPolicy.id\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=lambda: datetime.now(UTC))\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# #endregion ValidationRecord\n\n# #endregion LlmModels\n" }, { "contract_id": "ValidationPolicy", @@ -34246,7 +35119,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\n# @RELATION DEPENDS_ON -> [LLMProvider]\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 provider_id = Column(String, nullable=True) # Reference to LLMProvider.id\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# #endregion ValidationPolicy\n" + "body": "# #region ValidationPolicy [TYPE Class]\n# @BRIEF Defines a scheduled rule for validating a group of dashboards within an execution window.\n# @RELATION DEPENDS_ON -> [LLMProvider]\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 provider_id = Column(String, nullable=True) # Reference to LLMProvider.id\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=lambda: datetime.now(UTC))\n updated_at = Column(DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))\n# #endregion ValidationPolicy\n" }, { "contract_id": "LLMProvider", @@ -34263,7 +35136,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMProvider [TYPE Class]\n# @BRIEF 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 is_multimodal = Column(Boolean, default=False)\n created_at = Column(DateTime, default=datetime.utcnow)\n# #endregion LLMProvider\n" + "body": "# #region LLMProvider [TYPE Class]\n# @BRIEF 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 is_multimodal = Column(Boolean, default=False)\n created_at = Column(DateTime, default=lambda: datetime.now(UTC))\n# #endregion LLMProvider\n" }, { "contract_id": "ValidationRecord", @@ -34280,7 +35153,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationRecord [TYPE Class]\n# @BRIEF 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 policy_id = Column(String, nullable=True, index=True) # Reference to ValidationPolicy.id\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# #endregion ValidationRecord\n" + "body": "# #region ValidationRecord [TYPE Class]\n# @BRIEF 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 policy_id = Column(String, nullable=True, index=True) # Reference to ValidationPolicy.id\n dashboard_id = Column(String, nullable=False, index=True)\n environment_id = Column(String, nullable=True, index=True)\n timestamp = Column(DateTime, default=lambda: datetime.now(UTC))\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# #endregion ValidationRecord\n" }, { "contract_id": "MaintenanceModels", @@ -34313,7 +35186,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, maintenance, banner, event, settings, model]\n# @BRIEF SQLAlchemy models for the Maintenance Banner feature: event, banner, dashboard state, and settings.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT MaintenanceDashboardBanner has unique partial index on (environment_id, dashboard_id) WHERE status='active'\n# @INVARIANT MaintenanceSettings is a singleton row (id='default') enforced by CheckConstraint\n\nimport enum\nimport uuid\n\nfrom sqlalchemy import (\n CheckConstraint,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Index,\n Integer,\n JSON,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\nfrom sqlalchemy.sql import func\n\nfrom .mapping import Base\n\n\n# #region MaintenanceEventStatus [C:1] [TYPE Class]\nclass MaintenanceEventStatus(str, enum.Enum):\n PENDING = \"pending\"\n APPLYING = \"applying\"\n ACTIVE = \"active\"\n PARTIAL = \"partial\"\n ENDING = \"ending\"\n COMPLETED = \"completed\"\n FAILED = \"failed\"\n# #endregion MaintenanceEventStatus\n\n\n# #region MaintenanceDashboardBannerStatus [C:1] [TYPE Class]\nclass MaintenanceDashboardBannerStatus(str, enum.Enum):\n ACTIVE = \"active\"\n REMOVED = \"removed\"\n# #endregion MaintenanceDashboardBannerStatus\n\n\n# #region MaintenanceDashboardStateStatus [C:1] [TYPE Class]\nclass MaintenanceDashboardStateStatus(str, enum.Enum):\n PENDING_APPLY = \"pending_apply\"\n ACTIVE = \"active\"\n REMOVING = \"removing\"\n REMOVED = \"removed\"\n APPLY_FAILED = \"apply_failed\"\n REMOVAL_FAILED = \"removal_failed\"\n# #endregion MaintenanceDashboardStateStatus\n\n\n# #region DashboardScope [C:1] [TYPE Class]\nclass DashboardScope(str, enum.Enum):\n PUBLISHED_ONLY = \"published_only\"\n DRAFT_ONLY = \"draft_only\"\n ALL = \"all\"\n# #endregion DashboardScope\n\n\n# #region MaintenanceEvent [C:1] [TYPE Class]\n# @BRIEF A record of a maintenance event with table list, time window, status, and linked dashboard states.\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardState]\nclass MaintenanceEvent(Base):\n __tablename__ = \"maintenance_events\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n task_id = Column(String, nullable=True, index=True)\n environment_id = Column(String, nullable=False)\n tables = Column(JSON, nullable=False)\n start_time = Column(DateTime(timezone=True), nullable=False)\n end_time = Column(DateTime(timezone=True), nullable=True)\n message = Column(Text, nullable=True)\n status = Column(\n SQLEnum(MaintenanceEventStatus),\n nullable=False,\n default=MaintenanceEventStatus.PENDING,\n index=True,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n dashboard_states = relationship(\n \"MaintenanceDashboardState\",\n back_populates=\"maintenance_event\",\n cascade=\"all, delete-orphan\",\n )\n# #endregion MaintenanceEvent\n\n\n# #region MaintenanceDashboardBanner [C:1] [TYPE Class]\n# @BRIEF The canonical \"one chart per dashboard\" entity. Unique partial index enforces single active banner per dashboard.\n# @INVARIANT Unique partial index (environment_id, dashboard_id) WHERE status='active'\nclass MaintenanceDashboardBanner(Base):\n __tablename__ = \"maintenance_dashboard_banners\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n environment_id = Column(String, nullable=False)\n dashboard_id = Column(Integer, nullable=False, index=True)\n chart_id = Column(Integer, nullable=True)\n banner_text = Column(Text, nullable=True)\n status = Column(\n SQLEnum(MaintenanceDashboardBannerStatus),\n nullable=False,\n default=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n dashboard_states = relationship(\n \"MaintenanceDashboardState\", back_populates=\"banner\"\n )\n\n __table_args__ = (\n Index(\n \"ix_banner_unique_active\",\n \"environment_id\",\n \"dashboard_id\",\n unique=True,\n postgresql_where=(status == \"active\"),\n ),\n )\n# #endregion MaintenanceDashboardBanner\n\n\n# #region MaintenanceDashboardState [C:1] [TYPE Class]\n# @BRIEF Links a maintenance event to a dashboard via a banner. Tracks per-dashboard-per-event lifecycle.\n# @RELATION DEPENDS_ON -> [MaintenanceEvent]\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardBanner]\nclass MaintenanceDashboardState(Base):\n __tablename__ = \"maintenance_dashboard_states\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n event_id = Column(\n String, ForeignKey(\"maintenance_events.id\"), nullable=False, index=True\n )\n dashboard_id = Column(Integer, nullable=False, index=True)\n banner_id = Column(\n String,\n ForeignKey(\"maintenance_dashboard_banners.id\"),\n nullable=True,\n )\n status = Column(\n SQLEnum(MaintenanceDashboardStateStatus),\n nullable=False,\n default=MaintenanceDashboardStateStatus.PENDING_APPLY,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n maintenance_event = relationship(\n \"MaintenanceEvent\", back_populates=\"dashboard_states\"\n )\n banner = relationship(\n \"MaintenanceDashboardBanner\", back_populates=\"dashboard_states\"\n )\n\n __table_args__ = (\n Index(\"ix_mds_dashboard_status\", \"dashboard_id\", \"status\"),\n )\n# #endregion MaintenanceDashboardState\n\n\n# #region MaintenanceSettings [C:1] [TYPE Class]\n# @BRIEF Single-row maintenance mode configuration. Enforces singleton via CheckConstraint(id='default').\n# @INVARIANT id must always be 'default' — enforced by CheckConstraint\nclass MaintenanceSettings(Base):\n __tablename__ = \"maintenance_settings\"\n\n id = Column(String, primary_key=True, default=\"default\")\n target_environment_id = Column(String, nullable=False)\n display_timezone = Column(String, nullable=False, default=\"UTC\")\n banner_template = Column(\n Text,\n nullable=False,\n default=(\n '
\\n\\n'\n \"## ⚠️ Технические работы\\n\\n\"\n \"{message}\\n\\n\"\n \"**Начало:** {start_time}\\n\"\n \"**Конец:** {end_time}\\n\\n\"\n \"*Данные могут быть неполными или временно недоступны.*\\n\\n\"\n \"
\"\n ),\n )\n dashboard_scope = Column(\n SQLEnum(DashboardScope),\n nullable=False,\n default=DashboardScope.PUBLISHED_ONLY,\n )\n excluded_dashboard_ids = Column(JSON, nullable=False, default=list)\n forced_dashboard_ids = Column(JSON, nullable=False, default=list)\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n __table_args__ = (\n CheckConstraint(\"id = 'default'\", name=\"ck_settings_singleton\"),\n )\n# #endregion MaintenanceSettings\n\n# #endregion MaintenanceModels\n" + "body": "# #region MaintenanceModels [C:2] [TYPE Module] [SEMANTICS sqlalchemy, maintenance, banner, event, settings, model]\n# @BRIEF SQLAlchemy models for the Maintenance Banner feature: event, banner, dashboard state, and settings.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]\n# @RELATION DEPENDS_ON -> [MappingModels]\n# @INVARIANT MaintenanceDashboardBanner has unique partial index on (environment_id, dashboard_id) WHERE status='active'\n# @INVARIANT MaintenanceSettings is a singleton row (id='default') enforced by CheckConstraint\n\nimport enum\nimport uuid\n\nfrom sqlalchemy import (\n JSON,\n CheckConstraint,\n Column,\n DateTime,\n Enum as SQLEnum,\n ForeignKey,\n Index,\n Integer,\n String,\n Text,\n)\nfrom sqlalchemy.orm import relationship\nfrom sqlalchemy.sql import func\n\nfrom .mapping import Base\n\n\n# #region MaintenanceEventStatus [C:1] [TYPE Class]\nclass MaintenanceEventStatus(str, enum.Enum):\n PENDING = \"pending\"\n APPLYING = \"applying\"\n ACTIVE = \"active\"\n PARTIAL = \"partial\"\n ENDING = \"ending\"\n COMPLETED = \"completed\"\n FAILED = \"failed\"\n# #endregion MaintenanceEventStatus\n\n\n# #region MaintenanceDashboardBannerStatus [C:1] [TYPE Class]\nclass MaintenanceDashboardBannerStatus(str, enum.Enum):\n ACTIVE = \"active\"\n REMOVED = \"removed\"\n# #endregion MaintenanceDashboardBannerStatus\n\n\n# #region MaintenanceDashboardStateStatus [C:1] [TYPE Class]\nclass MaintenanceDashboardStateStatus(str, enum.Enum):\n PENDING_APPLY = \"pending_apply\"\n ACTIVE = \"active\"\n REMOVING = \"removing\"\n REMOVED = \"removed\"\n APPLY_FAILED = \"apply_failed\"\n REMOVAL_FAILED = \"removal_failed\"\n# #endregion MaintenanceDashboardStateStatus\n\n\n# #region DashboardScope [C:1] [TYPE Class]\nclass DashboardScope(str, enum.Enum):\n PUBLISHED_ONLY = \"published_only\"\n DRAFT_ONLY = \"draft_only\"\n ALL = \"all\"\n# #endregion DashboardScope\n\n\n# #region MaintenanceEvent [C:1] [TYPE Class]\n# @BRIEF A record of a maintenance event with table list, time window, status, and linked dashboard states.\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardState]\nclass MaintenanceEvent(Base):\n __tablename__ = \"maintenance_events\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n task_id = Column(String, nullable=True, index=True)\n environment_id = Column(String, nullable=False)\n tables = Column(JSON, nullable=False)\n start_time = Column(DateTime(timezone=True), nullable=False)\n end_time = Column(DateTime(timezone=True), nullable=True)\n message = Column(Text, nullable=True)\n status = Column(\n SQLEnum(MaintenanceEventStatus),\n nullable=False,\n default=MaintenanceEventStatus.PENDING,\n index=True,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n dashboard_states = relationship(\n \"MaintenanceDashboardState\",\n back_populates=\"maintenance_event\",\n cascade=\"all, delete-orphan\",\n )\n# #endregion MaintenanceEvent\n\n\n# #region MaintenanceDashboardBanner [C:1] [TYPE Class]\n# @BRIEF The canonical \"one chart per dashboard\" entity. Unique partial index enforces single active banner per dashboard.\n# @INVARIANT Unique partial index (environment_id, dashboard_id) WHERE status='active'\nclass MaintenanceDashboardBanner(Base):\n __tablename__ = \"maintenance_dashboard_banners\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n environment_id = Column(String, nullable=False)\n dashboard_id = Column(Integer, nullable=False, index=True)\n chart_id = Column(Integer, nullable=True)\n banner_text = Column(Text, nullable=True)\n status = Column(\n SQLEnum(MaintenanceDashboardBannerStatus),\n nullable=False,\n default=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n dashboard_states = relationship(\n \"MaintenanceDashboardState\", back_populates=\"banner\"\n )\n\n __table_args__ = (\n Index(\n \"ix_banner_unique_active\",\n \"environment_id\",\n \"dashboard_id\",\n unique=True,\n postgresql_where=(status == \"active\"),\n ),\n )\n# #endregion MaintenanceDashboardBanner\n\n\n# #region MaintenanceDashboardState [C:1] [TYPE Class]\n# @BRIEF Links a maintenance event to a dashboard via a banner. Tracks per-dashboard-per-event lifecycle.\n# @RELATION DEPENDS_ON -> [MaintenanceEvent]\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardBanner]\nclass MaintenanceDashboardState(Base):\n __tablename__ = \"maintenance_dashboard_states\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()), index=True)\n event_id = Column(\n String, ForeignKey(\"maintenance_events.id\"), nullable=False, index=True\n )\n dashboard_id = Column(Integer, nullable=False, index=True)\n banner_id = Column(\n String,\n ForeignKey(\"maintenance_dashboard_banners.id\"),\n nullable=True,\n )\n status = Column(\n SQLEnum(MaintenanceDashboardStateStatus),\n nullable=False,\n default=MaintenanceDashboardStateStatus.PENDING_APPLY,\n )\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n maintenance_event = relationship(\n \"MaintenanceEvent\", back_populates=\"dashboard_states\"\n )\n banner = relationship(\n \"MaintenanceDashboardBanner\", back_populates=\"dashboard_states\"\n )\n\n __table_args__ = (\n Index(\"ix_mds_dashboard_status\", \"dashboard_id\", \"status\"),\n )\n# #endregion MaintenanceDashboardState\n\n\n# #region MaintenanceSettings [C:1] [TYPE Class]\n# @BRIEF Single-row maintenance mode configuration. Enforces singleton via CheckConstraint(id='default').\n# @INVARIANT id must always be 'default' — enforced by CheckConstraint\nclass MaintenanceSettings(Base):\n __tablename__ = \"maintenance_settings\"\n\n id = Column(String, primary_key=True, default=\"default\")\n target_environment_id = Column(String, nullable=False)\n display_timezone = Column(String, nullable=False, default=\"UTC\")\n banner_template = Column(\n Text,\n nullable=False,\n default=(\n '
\\n\\n'\n \"## ⚠️ Технические работы\\n\\n\"\n \"{message}\\n\\n\"\n \"**Начало:** {start_time}\\n\"\n \"**Конец:** {end_time}\\n\\n\"\n \"*Данные могут быть неполными или временно недоступны.*\\n\\n\"\n \"
\"\n ),\n )\n dashboard_scope = Column(\n SQLEnum(DashboardScope),\n nullable=False,\n default=DashboardScope.PUBLISHED_ONLY,\n )\n excluded_dashboard_ids = Column(JSON, nullable=False, default=list)\n forced_dashboard_ids = Column(JSON, nullable=False, default=list)\n updated_at = Column(\n DateTime(timezone=True), server_default=func.now(), onupdate=func.now()\n )\n\n __table_args__ = (\n CheckConstraint(\"id = 'default'\", name=\"ck_settings_singleton\"),\n )\n# #endregion MaintenanceSettings\n\n# #endregion MaintenanceModels\n" }, { "contract_id": "MaintenanceEventStatus", @@ -34502,7 +35375,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MappingModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, mapping, model, schema, resource-type]\n#\n# @BRIEF Defines the database schema for environment metadata and database mappings using SQLAlchemy.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]\n\n#\n# @INVARIANT All primary keys are UUID strings.\n# CONSTRAINT: source_env_id and target_env_id must be valid environment IDs.\n# @PRE Database engine initialized\n# @POST Mapping ORM models registered with UUID primary keys\n# @SIDE_EFFECT Defines environment/database/resource mapping tables\n# @DATA_CONTRACT MappingData -> MappingRecord\n\nimport enum\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, Enum as SQLEnum, ForeignKey, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.sql import func\n\n# #region Base [C:1] [TYPE Class]\n# @BRIEF SQLAlchemy declarative base for all domain models.\n# #endregion Base\nBase = declarative_base()\n\n# #region ResourceType [C:1] [TYPE Class]\n# @BRIEF Enumeration of possible Superset resource types for ID mapping.\nclass ResourceType(str, enum.Enum):\n CHART = \"chart\"\n DATASET = \"dataset\"\n DASHBOARD = \"dashboard\"\n# #endregion ResourceType\n\n\n# #region MigrationStatus [C:1] [TYPE Class]\n# @BRIEF 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# #endregion MigrationStatus\n\n# #region Environment [C:3] [TYPE Class]\n# @BRIEF Represents a Superset instance environment.\n# @RELATION DEPENDS_ON -> MappingModels\nclass Environment(Base):\n __tablename__ = \"environments\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n url = Column(String, nullable=False)\n credentials_id = Column(String, nullable=False)\n# #endregion Environment\n\n# #region DatabaseMapping [C:3] [TYPE Class]\n# @BRIEF Represents a mapping between source and target databases.\nclass DatabaseMapping(Base):\n __tablename__ = \"database_mappings\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n source_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_db_uuid = Column(String, nullable=False)\n target_db_uuid = Column(String, nullable=False)\n source_db_name = Column(String, nullable=False)\n target_db_name = Column(String, nullable=False)\n engine = Column(String, nullable=True)\n# #endregion DatabaseMapping\n\n# #region MigrationJob [C:2] [TYPE Class]\n# @BRIEF Represents a single migration execution job.\nclass MigrationJob(Base):\n __tablename__ = \"migration_jobs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n source_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n status = Column(SQLEnum(MigrationStatus), default=MigrationStatus.PENDING)\n replace_db = Column(Boolean, default=False)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n# #endregion MigrationJob\n\n# #region ResourceMapping [C:3] [TYPE Class]\n# @BRIEF Maps a universal UUID for a resource to its actual ID on a specific environment.\n# @TEST_DATA: resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n# @RELATION DEPENDS_ON -> MappingModels\nclass ResourceMapping(Base):\n __tablename__ = \"resource_mappings\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n resource_type = Column(SQLEnum(ResourceType), nullable=False)\n uuid = Column(String, nullable=False)\n remote_integer_id = Column(String, nullable=False) # Stored as string to handle potentially large or composite IDs safely, though Superset usually uses integers.\n resource_name = Column(String, nullable=True) # Used for UI display\n last_synced_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# #endregion ResourceMapping\n\n# #endregion MappingModels\n" + "body": "# #region MappingModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, mapping, model, schema, resource-type]\n#\n# @BRIEF Defines the database schema for environment metadata and database mappings using SQLAlchemy.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]\n\n#\n# @INVARIANT All primary keys are UUID strings.\n# CONSTRAINT: source_env_id and target_env_id must be valid environment IDs.\n# @PRE Database engine initialized\n# @POST Mapping ORM models registered with UUID primary keys\n# @SIDE_EFFECT Defines environment/database/resource mapping tables\n# @DATA_CONTRACT MappingData -> MappingRecord\n\nimport enum\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, Enum as SQLEnum, ForeignKey, String\nfrom sqlalchemy.orm import declarative_base\nfrom sqlalchemy.sql import func\n\n# #region Base [C:1] [TYPE Class]\n# @BRIEF SQLAlchemy declarative base for all domain models.\n# #endregion Base\nBase = declarative_base()\n\n# #region ResourceType [C:1] [TYPE Class]\n# @BRIEF Enumeration of possible Superset resource types for ID mapping.\nclass ResourceType(str, enum.Enum):\n CHART = \"chart\"\n DATASET = \"dataset\"\n DASHBOARD = \"dashboard\"\n# #endregion ResourceType\n\n\n# #region MigrationStatus [C:1] [TYPE Class]\n# @BRIEF 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# #endregion MigrationStatus\n\n# #region Environment [C:3] [TYPE Class]\n# @BRIEF Represents a Superset instance environment.\n# @RELATION DEPENDS_ON -> MappingModels\nclass Environment(Base):\n __tablename__ = \"environments\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n name = Column(String, nullable=False)\n url = Column(String, nullable=False)\n credentials_id = Column(String, nullable=False)\n# #endregion Environment\n\n# #region DatabaseMapping [C:3] [TYPE Class]\n# @BRIEF Represents a mapping between source and target databases.\nclass DatabaseMapping(Base):\n __tablename__ = \"database_mappings\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n source_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n source_db_uuid = Column(String, nullable=False)\n target_db_uuid = Column(String, nullable=False)\n source_db_name = Column(String, nullable=False)\n target_db_name = Column(String, nullable=False)\n engine = Column(String, nullable=True)\n# #endregion DatabaseMapping\n\n# #region MigrationJob [C:2] [TYPE Class]\n# @BRIEF Represents a single migration execution job.\nclass MigrationJob(Base):\n __tablename__ = \"migration_jobs\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n source_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n target_env_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n status = Column(SQLEnum(MigrationStatus), default=MigrationStatus.PENDING)\n replace_db = Column(Boolean, default=False)\n created_at = Column(DateTime(timezone=True), server_default=func.now())\n# #endregion MigrationJob\n\n# #region ResourceMapping [C:3] [TYPE Class]\n# @BRIEF Maps a universal UUID for a resource to its actual ID on a specific environment.\n# @TEST_DATA: resource_mapping_record -> {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n# @RELATION DEPENDS_ON -> MappingModels\nclass ResourceMapping(Base):\n __tablename__ = \"resource_mappings\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n environment_id = Column(String, ForeignKey(\"environments.id\", ondelete=\"CASCADE\"), nullable=False)\n resource_type = Column(SQLEnum(ResourceType), nullable=False)\n uuid = Column(String, nullable=False)\n remote_integer_id = Column(String, nullable=False) # Stored as string to handle potentially large or composite IDs safely, though Superset usually uses integers.\n resource_name = Column(String, nullable=True) # Used for UI display\n last_synced_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())\n# #endregion ResourceMapping\n\n# #endregion MappingModels\n" }, { "contract_id": "Base", @@ -34637,7 +35510,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, model, schema, git, dashboard]\n#\n# @BRIEF Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthModels]\n# @RELATION INHERITS -> [MappingModels]\n#\n# @INVARIANT Exactly one preference row exists per user_id.\n# @INVARIANT Sensitive Git token is stored encrypted and never returned in plaintext.\n# @PRE Database engine initialized\n# @POST Profile ORM models registered\n# @SIDE_EFFECT Defines user profile/preference tables\n# @DATA_CONTRACT ProfileData -> PreferenceRecord\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, ForeignKey, String\nfrom sqlalchemy.orm import relationship\n\nfrom .mapping import Base\n\n\n# #region UserDashboardPreference [C:3] [TYPE Class]\n# @BRIEF Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user.\n# @RELATION INHERITS -> [MappingModels]\nclass UserDashboardPreference(Base):\n __tablename__ = \"user_dashboard_preferences\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False, unique=True, index=True)\n\n superset_username = Column(String, nullable=True)\n superset_username_normalized = Column(String, nullable=True, index=True)\n\n show_only_my_dashboards = Column(Boolean, nullable=False, default=False)\n show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)\n\n git_username = Column(String, nullable=True)\n git_email = Column(String, nullable=True)\n git_personal_access_token_encrypted = Column(String, nullable=True)\n\n start_page = Column(String, nullable=False, default=\"dashboards\")\n auto_open_task_drawer = Column(Boolean, nullable=False, default=True)\n dashboards_table_density = Column(String, nullable=False, default=\"comfortable\")\n\n telegram_id = Column(String, nullable=True)\n email_address = Column(String, nullable=True)\n notify_on_fail = Column(Boolean, nullable=False, default=True)\n\n created_at = Column(DateTime, nullable=False, default=datetime.utcnow)\n updated_at = Column(\n DateTime,\n nullable=False,\n default=datetime.utcnow,\n onupdate=datetime.utcnow,\n )\n\n user = relationship(\"User\")\n# #endregion UserDashboardPreference\n\n# #endregion ProfileModels\n" + "body": "# #region ProfileModels [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, model, schema, git, dashboard]\n#\n# @BRIEF Defines persistent per-user profile settings for dashboard filter, Git identity/token, and UX preferences.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthModels]\n# @RELATION INHERITS -> [MappingModels]\n#\n# @INVARIANT Exactly one preference row exists per user_id.\n# @INVARIANT Sensitive Git token is stored encrypted and never returned in plaintext.\n# @PRE Database engine initialized\n# @POST Profile ORM models registered\n# @SIDE_EFFECT Defines user profile/preference tables\n# @DATA_CONTRACT ProfileData -> PreferenceRecord\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy import Boolean, Column, DateTime, ForeignKey, String\nfrom sqlalchemy.orm import relationship\n\nfrom .mapping import Base\n\n\n# #region UserDashboardPreference [C:3] [TYPE Class]\n# @BRIEF Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user.\n# @RELATION INHERITS -> [MappingModels]\nclass UserDashboardPreference(Base):\n __tablename__ = \"user_dashboard_preferences\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False, unique=True, index=True)\n\n superset_username = Column(String, nullable=True)\n superset_username_normalized = Column(String, nullable=True, index=True)\n\n show_only_my_dashboards = Column(Boolean, nullable=False, default=False)\n show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)\n\n git_username = Column(String, nullable=True)\n git_email = Column(String, nullable=True)\n git_personal_access_token_encrypted = Column(String, nullable=True)\n\n start_page = Column(String, nullable=False, default=\"dashboards\")\n auto_open_task_drawer = Column(Boolean, nullable=False, default=True)\n dashboards_table_density = Column(String, nullable=False, default=\"comfortable\")\n\n telegram_id = Column(String, nullable=True)\n email_address = Column(String, nullable=True)\n notify_on_fail = Column(Boolean, nullable=False, default=True)\n\n created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n updated_at = Column(\n DateTime,\n nullable=False,\n default=lambda: datetime.now(UTC),\n onupdate=lambda: datetime.now(UTC),\n )\n\n user = relationship(\"User\")\n# #endregion UserDashboardPreference\n\n# #endregion ProfileModels\n" }, { "contract_id": "UserDashboardPreference", @@ -34662,7 +35535,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region UserDashboardPreference [C:3] [TYPE Class]\n# @BRIEF Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user.\n# @RELATION INHERITS -> [MappingModels]\nclass UserDashboardPreference(Base):\n __tablename__ = \"user_dashboard_preferences\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False, unique=True, index=True)\n\n superset_username = Column(String, nullable=True)\n superset_username_normalized = Column(String, nullable=True, index=True)\n\n show_only_my_dashboards = Column(Boolean, nullable=False, default=False)\n show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)\n\n git_username = Column(String, nullable=True)\n git_email = Column(String, nullable=True)\n git_personal_access_token_encrypted = Column(String, nullable=True)\n\n start_page = Column(String, nullable=False, default=\"dashboards\")\n auto_open_task_drawer = Column(Boolean, nullable=False, default=True)\n dashboards_table_density = Column(String, nullable=False, default=\"comfortable\")\n\n telegram_id = Column(String, nullable=True)\n email_address = Column(String, nullable=True)\n notify_on_fail = Column(Boolean, nullable=False, default=True)\n\n created_at = Column(DateTime, nullable=False, default=datetime.utcnow)\n updated_at = Column(\n DateTime,\n nullable=False,\n default=datetime.utcnow,\n onupdate=datetime.utcnow,\n )\n\n user = relationship(\"User\")\n# #endregion UserDashboardPreference\n" + "body": "# #region UserDashboardPreference [C:3] [TYPE Class]\n# @BRIEF Stores Superset username binding and default \"my dashboards\" toggle for one authenticated user.\n# @RELATION INHERITS -> [MappingModels]\nclass UserDashboardPreference(Base):\n __tablename__ = \"user_dashboard_preferences\"\n\n id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n user_id = Column(String, ForeignKey(\"users.id\"), nullable=False, unique=True, index=True)\n\n superset_username = Column(String, nullable=True)\n superset_username_normalized = Column(String, nullable=True, index=True)\n\n show_only_my_dashboards = Column(Boolean, nullable=False, default=False)\n show_only_slug_dashboards = Column(Boolean, nullable=False, default=True)\n\n git_username = Column(String, nullable=True)\n git_email = Column(String, nullable=True)\n git_personal_access_token_encrypted = Column(String, nullable=True)\n\n start_page = Column(String, nullable=False, default=\"dashboards\")\n auto_open_task_drawer = Column(Boolean, nullable=False, default=True)\n dashboards_table_density = Column(String, nullable=False, default=\"comfortable\")\n\n telegram_id = Column(String, nullable=True)\n email_address = Column(String, nullable=True)\n notify_on_fail = Column(Boolean, nullable=False, default=True)\n\n created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))\n updated_at = Column(\n DateTime,\n nullable=False,\n default=lambda: datetime.now(UTC),\n onupdate=lambda: datetime.now(UTC),\n )\n\n user = relationship(\"User\")\n# #endregion UserDashboardPreference\n" }, { "contract_id": "ReportModels", @@ -34852,7 +35725,7 @@ "contract_type": "Module", "file_path": "backend/src/models/storage.py", "start_line": 1, - "end_line": 35, + "end_line": 37, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -34862,14 +35735,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region StorageModels [TYPE Module] [SEMANTICS pydantic, storage, model, backup, config]\n# @RATIONALE Fixed typo 'repositorys' → 'repositories' in FileCategory.REPOSITORY and StorageConfig.repo_path default. Breaking change documented for existing installations.\nfrom datetime import datetime\nfrom enum import Enum\nfrom pydantic import BaseModel, Field\n\n# #region FileCategory [C:1] [TYPE Class]\n# @BRIEF Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositories\"\n# #endregion FileCategory\n\n# #region StorageConfig [C:1] [TYPE Class]\n# @BRIEF Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"/app/storage\", 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=\"repositories\", 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# #endregion StorageConfig\n\n# #region StoredFile [C:1] [TYPE Class]\n# @BRIEF 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: str | None = Field(None, description=\"MIME type of the file.\")\n# #endregion StoredFile\n# #endregion StorageModels\n" + "body": "# #region StorageModels [TYPE Module] [SEMANTICS pydantic, storage, model, backup, config]\n# @RATIONALE Fixed typo 'repositorys' → 'repositories' in FileCategory.REPOSITORY and StorageConfig.repo_path default. Breaking change documented for existing installations.\nfrom datetime import datetime\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\n# #region FileCategory [C:1] [TYPE Class]\n# @BRIEF Enumeration of supported file categories in the storage system.\nclass FileCategory(str, Enum):\n BACKUP = \"backups\"\n REPOSITORY = \"repositories\"\n# #endregion FileCategory\n\n# #region StorageConfig [C:1] [TYPE Class]\n# @BRIEF Configuration model for the storage system, defining paths and naming patterns.\nclass StorageConfig(BaseModel):\n root_path: str = Field(default=\"/app/storage\", 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=\"repositories\", 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# #endregion StorageConfig\n\n# #region StoredFile [C:1] [TYPE Class]\n# @BRIEF 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: str | None = Field(None, description=\"MIME type of the file.\")\n# #endregion StoredFile\n# #endregion StorageModels\n" }, { "contract_id": "FileCategory", "contract_type": "Class", "file_path": "backend/src/models/storage.py", - "start_line": 7, - "end_line": 12, + "start_line": 9, + "end_line": 14, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -34886,8 +35759,8 @@ "contract_id": "StorageConfig", "contract_type": "Class", "file_path": "backend/src/models/storage.py", - "start_line": 14, - "end_line": 23, + "start_line": 16, + "end_line": 25, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -34904,8 +35777,8 @@ "contract_id": "StoredFile", "contract_type": "Class", "file_path": "backend/src/models/storage.py", - "start_line": 25, - "end_line": 34, + "start_line": 27, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35488,7 +36361,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPluginModule [C:4] [TYPE Module] [SEMANTICS git, versioning, deploy, sync, superset, backup, transactional]\n#\n# @BRIEF Предоставляет плагин для версионирования и развертывания дашбордов Superset.\n# @LAYER Plugin\n# @RELATION INHERITS -> [PluginBase]\n# @RELATION DEPENDS_ON -> [GitService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [TaskContext]\n#\n# @INVARIANT Все операции с Git должны выполняться через GitService.\n# @CONSTRAINT: Плагин работает только с распакованными YAML-экспортами Superset.\n# @INVARIANT _handle_sync сохраняет backup управляемых директорий перед удалением;\n# при ошибке распаковки backup восстанавливается.\n\nimport io\nimport os\nfrom pathlib import Path\nimport shutil\nimport time\nfrom typing import Any\nimport zipfile\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger as app_logger\nfrom src.core.plugin_base import PluginBase\nfrom src.core.superset_client import SupersetClient\nfrom src.core.task_manager.context import TaskContext\nfrom src.services.git_service import GitService\n\n\n# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE shared config_manager доступен через src.dependencies.\n # @POST Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Используем shared config_manager из dependencies\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n except Exception as exc:\n app_logger.error(f\"GitPlugin: failed to get shared config_manager: {exc}\")\n self.config_manager = ConfigManager()\n app_logger.info(\"GitPlugin initialized with fallback ConfigManager.\")\n # endregion __init__\n\n @property\n # region id [C:1] [TYPE Function]\n def id(self) -> str:\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [C:1] [TYPE Function]\n def name(self) -> str:\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [C:1] [TYPE Function]\n def description(self) -> str:\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [C:1] [TYPE Function]\n def version(self) -> str:\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [C:1] [TYPE Function]\n def ui_route(self) -> str:\n return \"/git\"\n # endregion ui_route\n\n # region get_schema [TYPE 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 # endregion get_schema\n\n # region initialize [C:1] [TYPE Function]\n async def initialize(self):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n # endregion initialize\n\n # region execute [C:3] [TYPE Function]\n # @PURPOSE: Main task executor with TaskContext support.\n # @RELATION CALLS -> self._handle_sync\n # @RELATION CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: TaskContext | None = 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 # endregion execute\n\n # region _handle_sync [C:4] [TYPE Function] [SEMANTICS git,sync,backup,transactional]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий с backup/restore.\n # @PRE Репозиторий для дашборда должен существовать.\n # @POST Файлы в репозитории обновлены до текущего состояния в Superset.\n # Управляемые директории backup-ируются перед удалением; при ошибке распаковки backup восстанавливается.\n # @SIDE_EFFECT Изменяет файлы в локальной рабочей директории репозитория.\n # Создаёт временный backup в /tmp/ss-tools-backup-{dashboard_id}-{timestamp}/.\n # @RETURN Dict[str, str] - Результат синхронизации.\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: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\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 env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Fix 1: Create backup before deleting managed files\n backup_dir = Path(f\"/tmp/ss-tools-backup-{dashboard_id}-{time.time_ns()}\")\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.copytree(d_path, backup_dir / d)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n (backup_dir / f).parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(f_path, backup_dir / f)\n\n # Delete old managed files\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 try:\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\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 except Exception:\n # Fix 1: Restore backup on unzip failure\n if backup_dir.exists():\n for d in managed_dirs:\n src = backup_dir / d\n if src.exists():\n dst = repo_path / d\n if dst.exists():\n shutil.rmtree(dst)\n shutil.copytree(src, dst)\n for f in managed_files:\n src = backup_dir / f\n if src.exists():\n dst = repo_path / f\n dst.parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(src, dst)\n raise\n\n # Fix 1: Remove backup on success\n if backup_dir.exists():\n shutil.rmtree(backup_dir, ignore_errors=True)\n\n try:\n repo.git.add(A=True)\n app_logger.reason(\"Changes staged in git\", extra={\"src\": \"_handle_sync\"})\n except Exception as ge:\n app_logger.explore(f\"Failed to stage changes: {ge}\", extra={\"src\": \"_handle_sync\"})\n\n app_logger.info(f\"[_handle_sync][Coherence:OK] Dashboard {dashboard_id} synced successfully.\")\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n app_logger.error(f\"[_handle_sync][Coherence:Failed] Sync failed: {e}\")\n raise\n # endregion _handle_sync\n\n # region _handle_deploy [C:4] [TYPE Function] [SEMANTICS git,deploy,zip]\n # @PURPOSE: Packages repository into ZIP and imports into target Superset environment.\n # @POST Dashboard imported into target Superset.\n # @SIDE_EFFECT Creates and removes temporary ZIP file.\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 repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\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 arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n zip_buffer.seek(0)\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 client.authenticate()\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 app_logger.reason(f\"Importing dashboard to {env.name}\", extra={\"src\": \"_handle_deploy\"})\n result = client.import_dashboard(temp_zip_path)\n app_logger.info(f\"[_handle_deploy][Coherence:OK] Deployment successful for dashboard {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 app_logger.error(f\"[_handle_deploy][Coherence:Failed] Deployment failed: {e}\")\n raise\n # endregion _handle_deploy\n\n # region _get_env [C:4] [TYPE Function] [SEMANTICS env,config,strict]\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 # When env_id is explicitly provided and not found, raises ValueError (no fallback).\n # @RETURN Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: str | None = None):\n with belief_scope(\"GitPlugin._get_env\"):\n app_logger.reason(f\"Fetching environment for ID: {env_id}\", extra={\"src\": \"_get_env\"})\n\n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n app_logger.reflect(f\"Found environment by ID in ConfigManager: {env.name}\", extra={\"src\": \"_get_env\"})\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 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 app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\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\n # Fix 7: Strict check — if env_id was explicitly provided but not found, raise immediately\n if env_id:\n raise ValueError(f\"Environment '{env_id}' not found in ConfigManager or database\")\n finally:\n db.close()\n\n # Priority 3: fallback to first env only when no specific env_id was requested\n envs = self.config_manager.get_environments()\n if envs and not env_id:\n app_logger.reflect(f\"Using first environment from ConfigManager: {envs[0].name}\", extra={\"src\": \"_get_env\"})\n return envs[0]\n\n app_logger.error(f\"[_get_env][Coherence:Failed] No environments configured (searched config.json and DB). env_id={env_id}\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # endregion _get_env\n\n# #endregion GitPlugin\n# #endregion GitPluginModule\n" + "body": "# #region GitPluginModule [C:4] [TYPE Module] [SEMANTICS git, versioning, deploy, sync, superset, backup, transactional]\n#\n# @BRIEF Предоставляет плагин для версионирования и развертывания дашбордов Superset.\n# @LAYER Plugin\n# @RELATION INHERITS -> [PluginBase]\n# @RELATION DEPENDS_ON -> [GitService]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [TaskContext]\n#\n# @INVARIANT Все операции с Git должны выполняться через GitService.\n# @CONSTRAINT: Плагин работает только с распакованными YAML-экспортами Superset.\n# @INVARIANT _handle_sync сохраняет backup управляемых директорий перед удалением;\n# при ошибке распаковки backup восстанавливается.\n\nimport io\nimport os\nfrom pathlib import Path\nimport shutil\nimport time\nfrom typing import Any\nimport zipfile\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger as app_logger\nfrom src.core.plugin_base import PluginBase\nfrom src.core.superset_client import SupersetClient\nfrom src.core.task_manager.context import TaskContext\nfrom src.services.git_service import GitService\n\n\n# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE shared config_manager доступен через src.dependencies.\n # @POST Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Используем shared config_manager из dependencies\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n except Exception as exc:\n app_logger.error(f\"GitPlugin: failed to get shared config_manager: {exc}\")\n self.config_manager = ConfigManager()\n app_logger.info(\"GitPlugin initialized with fallback ConfigManager.\")\n # endregion __init__\n\n @property\n # region id [C:1] [TYPE Function]\n def id(self) -> str:\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [C:1] [TYPE Function]\n def name(self) -> str:\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [C:1] [TYPE Function]\n def description(self) -> str:\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [C:1] [TYPE Function]\n def version(self) -> str:\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [C:1] [TYPE Function]\n def ui_route(self) -> str:\n return \"/git\"\n # endregion ui_route\n\n # region get_schema [TYPE 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 # endregion get_schema\n\n # region initialize [C:1] [TYPE Function]\n async def initialize(self):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n # endregion initialize\n\n # region execute [C:3] [TYPE Function]\n # @PURPOSE: Main task executor with TaskContext support.\n # @RELATION CALLS -> self._handle_sync\n # @RELATION CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: TaskContext | None = 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 # endregion execute\n\n # region _handle_sync [C:4] [TYPE Function] [SEMANTICS git,sync,backup,transactional]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий с backup/restore.\n # @PRE Репозиторий для дашборда должен существовать.\n # @POST Файлы в репозитории обновлены до текущего состояния в Superset.\n # Управляемые директории backup-ируются перед удалением; при ошибке распаковки backup восстанавливается.\n # @SIDE_EFFECT Изменяет файлы в локальной рабочей директории репозитория.\n # Создаёт временный backup в /tmp/ss-tools-backup-{dashboard_id}-{timestamp}/.\n # @RETURN Dict[str, str] - Результат синхронизации.\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: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\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 env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Fix 1: Create backup before deleting managed files\n backup_dir = Path(f\"/tmp/ss-tools-backup-{dashboard_id}-{time.time_ns()}\")\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.copytree(d_path, backup_dir / d)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n (backup_dir / f).parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(f_path, backup_dir / f)\n\n # Delete old managed files\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 try:\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\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 except Exception:\n # Fix 1: Restore backup on unzip failure\n if backup_dir.exists():\n for d in managed_dirs:\n src = backup_dir / d\n if src.exists():\n dst = repo_path / d\n if dst.exists():\n shutil.rmtree(dst)\n shutil.copytree(src, dst)\n for f in managed_files:\n src = backup_dir / f\n if src.exists():\n dst = repo_path / f\n dst.parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(src, dst)\n raise\n\n # Fix 1: Remove backup on success\n if backup_dir.exists():\n shutil.rmtree(backup_dir, ignore_errors=True)\n\n try:\n repo.git.add(A=True, force=True)\n app_logger.reason(\"Changes staged in git (force)\", extra={\"src\": \"_handle_sync\"})\n except Exception as ge:\n app_logger.explore(f\"Failed to stage changes: {ge}\", extra={\"src\": \"_handle_sync\"})\n\n app_logger.info(f\"[_handle_sync][Coherence:OK] Dashboard {dashboard_id} synced successfully.\")\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n app_logger.error(f\"[_handle_sync][Coherence:Failed] Sync failed: {e}\")\n raise\n # endregion _handle_sync\n\n # region _handle_deploy [C:4] [TYPE Function] [SEMANTICS git,deploy,zip]\n # @PURPOSE: Packages repository into ZIP and imports into target Superset environment.\n # @POST Dashboard imported into target Superset.\n # @SIDE_EFFECT Creates and removes temporary ZIP file.\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 repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\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 arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n zip_buffer.seek(0)\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 client.authenticate()\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 app_logger.reason(f\"Importing dashboard to {env.name}\", extra={\"src\": \"_handle_deploy\"})\n result = client.import_dashboard(temp_zip_path)\n app_logger.info(f\"[_handle_deploy][Coherence:OK] Deployment successful for dashboard {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 app_logger.error(f\"[_handle_deploy][Coherence:Failed] Deployment failed: {e}\")\n raise\n # endregion _handle_deploy\n\n # region _get_env [C:4] [TYPE Function] [SEMANTICS env,config,strict]\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 # When env_id is explicitly provided and not found, raises ValueError (no fallback).\n # @RETURN Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: str | None = None):\n with belief_scope(\"GitPlugin._get_env\"):\n app_logger.reason(f\"Fetching environment for ID: {env_id}\", extra={\"src\": \"_get_env\"})\n\n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n app_logger.reflect(f\"Found environment by ID in ConfigManager: {env.name}\", extra={\"src\": \"_get_env\"})\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 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 app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\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\n # Fix 7: Strict check — if env_id was explicitly provided but not found, raise immediately\n if env_id:\n raise ValueError(f\"Environment '{env_id}' not found in ConfigManager or database\")\n finally:\n db.close()\n\n # Priority 3: fallback to first env only when no specific env_id was requested\n envs = self.config_manager.get_environments()\n if envs and not env_id:\n app_logger.reflect(f\"Using first environment from ConfigManager: {envs[0].name}\", extra={\"src\": \"_get_env\"})\n return envs[0]\n\n app_logger.error(f\"[_get_env][Coherence:Failed] No environments configured (searched config.json and DB). env_id={env_id}\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # endregion _get_env\n\n# #endregion GitPlugin\n# #endregion GitPluginModule\n" }, { "contract_id": "GitPlugin", @@ -35505,7 +36378,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE shared config_manager доступен через src.dependencies.\n # @POST Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Используем shared config_manager из dependencies\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n except Exception as exc:\n app_logger.error(f\"GitPlugin: failed to get shared config_manager: {exc}\")\n self.config_manager = ConfigManager()\n app_logger.info(\"GitPlugin initialized with fallback ConfigManager.\")\n # endregion __init__\n\n @property\n # region id [C:1] [TYPE Function]\n def id(self) -> str:\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [C:1] [TYPE Function]\n def name(self) -> str:\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [C:1] [TYPE Function]\n def description(self) -> str:\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [C:1] [TYPE Function]\n def version(self) -> str:\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [C:1] [TYPE Function]\n def ui_route(self) -> str:\n return \"/git\"\n # endregion ui_route\n\n # region get_schema [TYPE 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 # endregion get_schema\n\n # region initialize [C:1] [TYPE Function]\n async def initialize(self):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n # endregion initialize\n\n # region execute [C:3] [TYPE Function]\n # @PURPOSE: Main task executor with TaskContext support.\n # @RELATION CALLS -> self._handle_sync\n # @RELATION CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: TaskContext | None = 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 # endregion execute\n\n # region _handle_sync [C:4] [TYPE Function] [SEMANTICS git,sync,backup,transactional]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий с backup/restore.\n # @PRE Репозиторий для дашборда должен существовать.\n # @POST Файлы в репозитории обновлены до текущего состояния в Superset.\n # Управляемые директории backup-ируются перед удалением; при ошибке распаковки backup восстанавливается.\n # @SIDE_EFFECT Изменяет файлы в локальной рабочей директории репозитория.\n # Создаёт временный backup в /tmp/ss-tools-backup-{dashboard_id}-{timestamp}/.\n # @RETURN Dict[str, str] - Результат синхронизации.\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: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\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 env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Fix 1: Create backup before deleting managed files\n backup_dir = Path(f\"/tmp/ss-tools-backup-{dashboard_id}-{time.time_ns()}\")\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.copytree(d_path, backup_dir / d)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n (backup_dir / f).parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(f_path, backup_dir / f)\n\n # Delete old managed files\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 try:\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\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 except Exception:\n # Fix 1: Restore backup on unzip failure\n if backup_dir.exists():\n for d in managed_dirs:\n src = backup_dir / d\n if src.exists():\n dst = repo_path / d\n if dst.exists():\n shutil.rmtree(dst)\n shutil.copytree(src, dst)\n for f in managed_files:\n src = backup_dir / f\n if src.exists():\n dst = repo_path / f\n dst.parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(src, dst)\n raise\n\n # Fix 1: Remove backup on success\n if backup_dir.exists():\n shutil.rmtree(backup_dir, ignore_errors=True)\n\n try:\n repo.git.add(A=True)\n app_logger.reason(\"Changes staged in git\", extra={\"src\": \"_handle_sync\"})\n except Exception as ge:\n app_logger.explore(f\"Failed to stage changes: {ge}\", extra={\"src\": \"_handle_sync\"})\n\n app_logger.info(f\"[_handle_sync][Coherence:OK] Dashboard {dashboard_id} synced successfully.\")\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n app_logger.error(f\"[_handle_sync][Coherence:Failed] Sync failed: {e}\")\n raise\n # endregion _handle_sync\n\n # region _handle_deploy [C:4] [TYPE Function] [SEMANTICS git,deploy,zip]\n # @PURPOSE: Packages repository into ZIP and imports into target Superset environment.\n # @POST Dashboard imported into target Superset.\n # @SIDE_EFFECT Creates and removes temporary ZIP file.\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 repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\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 arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n zip_buffer.seek(0)\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 client.authenticate()\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 app_logger.reason(f\"Importing dashboard to {env.name}\", extra={\"src\": \"_handle_deploy\"})\n result = client.import_dashboard(temp_zip_path)\n app_logger.info(f\"[_handle_deploy][Coherence:OK] Deployment successful for dashboard {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 app_logger.error(f\"[_handle_deploy][Coherence:Failed] Deployment failed: {e}\")\n raise\n # endregion _handle_deploy\n\n # region _get_env [C:4] [TYPE Function] [SEMANTICS env,config,strict]\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 # When env_id is explicitly provided and not found, raises ValueError (no fallback).\n # @RETURN Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: str | None = None):\n with belief_scope(\"GitPlugin._get_env\"):\n app_logger.reason(f\"Fetching environment for ID: {env_id}\", extra={\"src\": \"_get_env\"})\n\n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n app_logger.reflect(f\"Found environment by ID in ConfigManager: {env.name}\", extra={\"src\": \"_get_env\"})\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 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 app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\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\n # Fix 7: Strict check — if env_id was explicitly provided but not found, raise immediately\n if env_id:\n raise ValueError(f\"Environment '{env_id}' not found in ConfigManager or database\")\n finally:\n db.close()\n\n # Priority 3: fallback to first env only when no specific env_id was requested\n envs = self.config_manager.get_environments()\n if envs and not env_id:\n app_logger.reflect(f\"Using first environment from ConfigManager: {envs[0].name}\", extra={\"src\": \"_get_env\"})\n return envs[0]\n\n app_logger.error(f\"[_get_env][Coherence:Failed] No environments configured (searched config.json and DB). env_id={env_id}\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # endregion _get_env\n\n# #endregion GitPlugin\n" + "body": "# #region GitPlugin [TYPE Class]\n# @BRIEF Реализация плагина Git Integration для управления версиями дашбордов.\nclass GitPlugin(PluginBase):\n\n # region __init__ [TYPE Function]\n # @PURPOSE: Инициализирует плагин и его зависимости.\n # @PRE shared config_manager доступен через src.dependencies.\n # @POST Инициализированы git_service и config_manager.\n def __init__(self):\n with belief_scope(\"GitPlugin.__init__\"):\n app_logger.info(\"Initializing GitPlugin.\")\n self.git_service = GitService()\n\n # Используем shared config_manager из dependencies\n try:\n from src.dependencies import config_manager\n self.config_manager = config_manager\n app_logger.info(\"GitPlugin initialized using shared config_manager.\")\n except Exception as exc:\n app_logger.error(f\"GitPlugin: failed to get shared config_manager: {exc}\")\n self.config_manager = ConfigManager()\n app_logger.info(\"GitPlugin initialized with fallback ConfigManager.\")\n # endregion __init__\n\n @property\n # region id [C:1] [TYPE Function]\n def id(self) -> str:\n return \"git-integration\"\n # endregion id\n\n @property\n # region name [C:1] [TYPE Function]\n def name(self) -> str:\n return \"Git Integration\"\n # endregion name\n\n @property\n # region description [C:1] [TYPE Function]\n def description(self) -> str:\n return \"Version control for Superset dashboards\"\n # endregion description\n\n @property\n # region version [C:1] [TYPE Function]\n def version(self) -> str:\n return \"0.1.0\"\n # endregion version\n\n @property\n # region ui_route [C:1] [TYPE Function]\n def ui_route(self) -> str:\n return \"/git\"\n # endregion ui_route\n\n # region get_schema [TYPE 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 # endregion get_schema\n\n # region initialize [C:1] [TYPE Function]\n async def initialize(self):\n app_logger.reason(\"Initializing Git Integration Plugin logic.\", extra={\"src\": \"GitPlugin.initialize\"})\n # endregion initialize\n\n # region execute [C:3] [TYPE Function]\n # @PURPOSE: Main task executor with TaskContext support.\n # @RELATION CALLS -> self._handle_sync\n # @RELATION CALLS -> self._handle_deploy\n async def execute(self, task_data: dict[str, Any], context: TaskContext | None = 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 # endregion execute\n\n # region _handle_sync [C:4] [TYPE Function] [SEMANTICS git,sync,backup,transactional]\n # @PURPOSE: Экспортирует дашборд из Superset и распаковывает в Git-репозиторий с backup/restore.\n # @PRE Репозиторий для дашборда должен существовать.\n # @POST Файлы в репозитории обновлены до текущего состояния в Superset.\n # Управляемые директории backup-ируются перед удалением; при ошибке распаковки backup восстанавливается.\n # @SIDE_EFFECT Изменяет файлы в локальной рабочей директории репозитория.\n # Создаёт временный backup в /tmp/ss-tools-backup-{dashboard_id}-{timestamp}/.\n # @RETURN Dict[str, str] - Результат синхронизации.\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: str | None = None, log=None, git_log=None, superset_log=None) -> dict[str, str]:\n with belief_scope(\"GitPlugin._handle_sync\"):\n try:\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 env = self._get_env(source_env_id)\n client = SupersetClient(env)\n client.authenticate()\n\n superset_log.info(f\"Exporting dashboard {dashboard_id} from {env.name}\")\n zip_bytes, _ = client.export_dashboard(dashboard_id)\n\n git_log.info(f\"Unpacking export to {repo_path}\")\n\n managed_dirs = [\"dashboards\", \"charts\", \"datasets\", \"databases\"]\n managed_files = [\"metadata.yaml\"]\n\n # Fix 1: Create backup before deleting managed files\n backup_dir = Path(f\"/tmp/ss-tools-backup-{dashboard_id}-{time.time_ns()}\")\n for d in managed_dirs:\n d_path = repo_path / d\n if d_path.exists() and d_path.is_dir():\n shutil.copytree(d_path, backup_dir / d)\n for f in managed_files:\n f_path = repo_path / f\n if f_path.exists():\n (backup_dir / f).parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(f_path, backup_dir / f)\n\n # Delete old managed files\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 try:\n with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:\n namelist = zf.namelist()\n if not namelist:\n raise ValueError(\"Export ZIP is empty\")\n root_folder = namelist[0].split('/')[0]\n git_log.info(f\"Detected root folder in ZIP: {root_folder}\")\n for member in zf.infolist():\n if member.filename.startswith(root_folder + \"/\") and len(member.filename) > len(root_folder) + 1:\n relative_path = member.filename[len(root_folder)+1:]\n target_path = repo_path / relative_path\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 except Exception:\n # Fix 1: Restore backup on unzip failure\n if backup_dir.exists():\n for d in managed_dirs:\n src = backup_dir / d\n if src.exists():\n dst = repo_path / d\n if dst.exists():\n shutil.rmtree(dst)\n shutil.copytree(src, dst)\n for f in managed_files:\n src = backup_dir / f\n if src.exists():\n dst = repo_path / f\n dst.parent.mkdir(parents=True, exist_ok=True)\n shutil.copy2(src, dst)\n raise\n\n # Fix 1: Remove backup on success\n if backup_dir.exists():\n shutil.rmtree(backup_dir, ignore_errors=True)\n\n try:\n repo.git.add(A=True, force=True)\n app_logger.reason(\"Changes staged in git (force)\", extra={\"src\": \"_handle_sync\"})\n except Exception as ge:\n app_logger.explore(f\"Failed to stage changes: {ge}\", extra={\"src\": \"_handle_sync\"})\n\n app_logger.info(f\"[_handle_sync][Coherence:OK] Dashboard {dashboard_id} synced successfully.\")\n return {\"status\": \"success\", \"message\": \"Dashboard synced and flattened in local repository\"}\n\n except Exception as e:\n app_logger.error(f\"[_handle_sync][Coherence:Failed] Sync failed: {e}\")\n raise\n # endregion _handle_sync\n\n # region _handle_deploy [C:4] [TYPE Function] [SEMANTICS git,deploy,zip]\n # @PURPOSE: Packages repository into ZIP and imports into target Superset environment.\n # @POST Dashboard imported into target Superset.\n # @SIDE_EFFECT Creates and removes temporary ZIP file.\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 repo = self.git_service.get_repo(dashboard_id)\n repo_path = Path(repo.working_dir)\n git_log.info(f\"Packing repository {repo_path} for deployment.\")\n zip_buffer = io.BytesIO()\n root_dir_name = f\"dashboard_export_{dashboard_id}\"\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 arcname = Path(root_dir_name) / file_path.relative_to(repo_path)\n zf.write(file_path, arcname)\n zip_buffer.seek(0)\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 client.authenticate()\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 app_logger.reason(f\"Importing dashboard to {env.name}\", extra={\"src\": \"_handle_deploy\"})\n result = client.import_dashboard(temp_zip_path)\n app_logger.info(f\"[_handle_deploy][Coherence:OK] Deployment successful for dashboard {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 app_logger.error(f\"[_handle_deploy][Coherence:Failed] Deployment failed: {e}\")\n raise\n # endregion _handle_deploy\n\n # region _get_env [C:4] [TYPE Function] [SEMANTICS env,config,strict]\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 # When env_id is explicitly provided and not found, raises ValueError (no fallback).\n # @RETURN Environment - Объект конфигурации окружения.\n def _get_env(self, env_id: str | None = None):\n with belief_scope(\"GitPlugin._get_env\"):\n app_logger.reason(f\"Fetching environment for ID: {env_id}\", extra={\"src\": \"_get_env\"})\n\n # Priority 1: ConfigManager (config.json)\n if env_id:\n env = self.config_manager.get_environment(env_id)\n if env:\n app_logger.reflect(f\"Found environment by ID in ConfigManager: {env.name}\", extra={\"src\": \"_get_env\"})\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 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 app_logger.reflect(f\"Found environment in DB: {db_env.name}\", extra={\"src\": \"_get_env\"})\n from src.core.config_models import Environment\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\n # Fix 7: Strict check — if env_id was explicitly provided but not found, raise immediately\n if env_id:\n raise ValueError(f\"Environment '{env_id}' not found in ConfigManager or database\")\n finally:\n db.close()\n\n # Priority 3: fallback to first env only when no specific env_id was requested\n envs = self.config_manager.get_environments()\n if envs and not env_id:\n app_logger.reflect(f\"Using first environment from ConfigManager: {envs[0].name}\", extra={\"src\": \"_get_env\"})\n return envs[0]\n\n app_logger.error(f\"[_get_env][Coherence:Failed] No environments configured (searched config.json and DB). env_id={env_id}\")\n raise ValueError(\"No environments configured. Please add a Superset Environment in Settings.\")\n # endregion _get_env\n\n# #endregion GitPlugin\n" }, { "contract_id": "LLMAnalysisPackage", @@ -35546,7 +36419,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMAnalysisModels [C:3] [TYPE Module] [SEMANTICS pydantic, llm, plugin, model, provider-type]\n# @BRIEF Define Pydantic models for LLM Analysis plugin.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LLMProviderType [TYPE Class]\n# @BRIEF Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n LITELLM = \"litellm\"\n# #endregion LLMProviderType\n\n# #region LLMProviderConfig [TYPE Class]\n# @BRIEF Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: str | None = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: str | None = None\n default_model: str\n is_active: bool = True\n is_multimodal: bool = False\n# #endregion LLMProviderConfig\n\n# #region ValidationStatus [TYPE Class]\n# @BRIEF Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# #endregion ValidationStatus\n\n# #region DetectedIssue [TYPE Class]\n# @BRIEF Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: str | None = None\n# #endregion DetectedIssue\n\n# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n\n# #endregion LLMAnalysisModels\n" + "body": "# #region LLMAnalysisModels [C:3] [TYPE Module] [SEMANTICS pydantic, llm, plugin, model, provider-type]\n# @BRIEF Define Pydantic models for LLM Analysis plugin.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import UTC, datetime\nfrom enum import Enum\n\nfrom pydantic import BaseModel, Field\n\n\n# #region LLMProviderType [TYPE Class]\n# @BRIEF Enum for supported LLM providers.\nclass LLMProviderType(str, Enum):\n OPENAI = \"openai\"\n OPENROUTER = \"openrouter\"\n KILO = \"kilo\"\n LITELLM = \"litellm\"\n# #endregion LLMProviderType\n\n# #region LLMProviderConfig [TYPE Class]\n# @BRIEF Configuration for an LLM provider.\nclass LLMProviderConfig(BaseModel):\n id: str | None = None\n provider_type: LLMProviderType\n name: str\n base_url: str\n api_key: str | None = None\n default_model: str\n is_active: bool = True\n is_multimodal: bool = False\n# #endregion LLMProviderConfig\n\n# #region ValidationStatus [TYPE Class]\n# @BRIEF Enum for dashboard validation status.\nclass ValidationStatus(str, Enum):\n PASS = \"PASS\"\n WARN = \"WARN\"\n FAIL = \"FAIL\"\n UNKNOWN = \"UNKNOWN\"\n# #endregion ValidationStatus\n\n# #region DetectedIssue [TYPE Class]\n# @BRIEF Model for a single issue detected during validation.\nclass DetectedIssue(BaseModel):\n severity: ValidationStatus\n message: str\n location: str | None = None\n# #endregion DetectedIssue\n\n# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n\n# #endregion LLMAnalysisModels\n" }, { "contract_id": "LLMProviderType", @@ -35631,7 +36504,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=datetime.utcnow)\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n" + "body": "# #region ValidationResult [TYPE Class]\n# @BRIEF Model for dashboard validation result.\nclass ValidationResult(BaseModel):\n id: str | None = None\n dashboard_id: str\n timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))\n status: ValidationStatus\n screenshot_path: str | None = None\n issues: list[DetectedIssue]\n summary: str\n raw_response: str | None = None\n# #endregion ValidationResult\n" }, { "contract_id": "LLMAnalysisPlugin", @@ -35683,7 +36556,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMAnalysisPlugin [C:5] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]\n# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.\n# @LAYER Plugin\n# @RELATION INHERITS -> [PluginBase]\n# @RELATION CALLS -> [ScreenshotService]\n# @RELATION CALLS -> [LLMClient]\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @INVARIANT All LLM interactions must be executed as asynchronous tasks.\n# @DATA_CONTRACT AnalysisRequest -> AnalysisResult\n\nfrom datetime import datetime, timedelta\nimport json\nimport os\nfrom typing import Any\n\nfrom ...core.database import SessionLocal\nfrom ...core.logger import belief_scope, logger\nfrom ...core.plugin_base import PluginBase\nfrom ...core.superset_client import SupersetClient\nfrom ...core.task_manager.context import TaskContext\nfrom ...models.llm import ValidationPolicy, ValidationRecord\nfrom ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n render_prompt,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...services.notifications.service import NotificationService\nfrom .models import DetectedIssue, LLMProviderType, ValidationResult, ValidationStatus\nfrom .service import LLMClient, ScreenshotService\n\n\n# #region _is_masked_or_invalid_api_key [TYPE Function]\n# @BRIEF Guards against placeholder or malformed API keys in runtime.\n# @PRE value may be None.\n# @POST Returns True when value cannot be used for authenticated provider calls.\ndef _is_masked_or_invalid_api_key(value: str | None) -> bool:\n key = (value or \"\").strip()\n if not key:\n return True\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return True\n # Most provider tokens are significantly longer; short values are almost always placeholders.\n return len(key) < 16\n# #endregion _is_masked_or_invalid_api_key\n\n# #region _json_safe_value [TYPE Function]\n# @BRIEF Recursively normalize payload values for JSON serialization.\n# @PRE value may be nested dict/list with datetime values.\n# @POST datetime values are converted to ISO strings.\ndef _json_safe_value(value: Any):\n if isinstance(value, datetime):\n return value.isoformat()\n if isinstance(value, dict):\n return {k: _json_safe_value(v) for k, v in value.items()}\n if isinstance(value, list):\n return [_json_safe_value(v) for v in value]\n return value\n# #endregion _json_safe_value\n\n# #region DashboardValidationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dashboard health analysis using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DashboardValidationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_dashboard_validation\"\n\n @property\n def name(self) -> str:\n return \"Dashboard LLM Validation\"\n\n @property\n def description(self) -> str:\n return \"Automated dashboard health analysis using multimodal 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 \"dashboard_id\": {\"type\": \"string\", \"title\": \"Dashboard ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dashboard_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DashboardValidationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dashboard validation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Validation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dashboard_id, environment_id, and provider_id.\n # @POST Returns a dictionary with validation results and persists them to the database.\n # @SIDE_EFFECT Captures a screenshot, calls LLM API, and writes to the database.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\n validation_started_at = datetime.utcnow()\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 llm_log = log.with_source(\"llm\") if context else log\n screenshot_log = log.with_source(\"screenshot\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dashboard_id_raw = params.get(\"dashboard_id\")\n dashboard_id = str(dashboard_id_raw) if dashboard_id_raw is not None else None\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n llm_log.debug(f\" Is Active: {db_provider.is_active}\")\n llm_log.debug(f\" Is Multimodal: {db_provider.is_multimodal}\")\n if not bool(db_provider.is_multimodal):\n raise ValueError(\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Capture Screenshot\n screenshot_service = ScreenshotService(env)\n\n storage_root = config_mgr.get_config().settings.storage.root_path\n screenshots_dir = os.path.join(storage_root, \"screenshots\")\n os.makedirs(screenshots_dir, exist_ok=True)\n\n filename = f\"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png\"\n screenshot_path = os.path.join(screenshots_dir, filename)\n\n screenshot_started_at = datetime.utcnow()\n screenshot_log.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)\n screenshot_log.debug(f\"Screenshot saved to: {screenshot_path}\")\n screenshot_finished_at = datetime.utcnow()\n\n # 4. Fetch Logs (from Environment /api/v1/log/)\n logs = []\n logs_fetch_started_at = datetime.utcnow()\n try:\n client = SupersetClient(env)\n\n # Calculate time window (last 24 hours)\n start_time = (datetime.now() - timedelta(hours=24)).isoformat()\n\n # Construct filter for logs\n # Note: We filter by dashboard_id matching the object\n query_params = {\n \"filters\": [\n {\"col\": \"dashboard_id\", \"opr\": \"eq\", \"value\": dashboard_id},\n {\"col\": \"dttm\", \"opr\": \"gt\", \"value\": start_time}\n ],\n \"order_column\": \"dttm\",\n \"order_direction\": \"desc\",\n \"page\": 0,\n \"page_size\": 100\n }\n\n superset_log.debug(f\"Fetching logs for dashboard {dashboard_id}\")\n response = client.network.request(\n method=\"GET\",\n endpoint=\"/log/\",\n params={\"q\": json.dumps(query_params)}\n )\n\n if isinstance(response, dict) and \"result\" in response:\n for item in response[\"result\"]:\n action = item.get(\"action\", \"unknown\")\n dttm = item.get(\"dttm\", \"\")\n details = item.get(\"json\", \"\")\n logs.append(f\"[{dttm}] {action}: {details}\")\n\n if not logs:\n logs = [\"No recent logs found for this dashboard.\"]\n superset_log.debug(\"No recent logs found for this dashboard\")\n\n except Exception as e:\n superset_log.warning(f\"Failed to fetch logs from environment: {e}\")\n logs = [f\"Error fetching remote logs: {e!s}\"]\n logs_fetch_finished_at = datetime.utcnow()\n\n # 5. Analyze with LLM\n llm_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 llm_log.info(f\"Analyzing dashboard {dashboard_id} with LLM\")\n llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n dashboard_prompt = llm_settings[\"prompts\"].get(\n \"dashboard_validation_prompt\",\n DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n )\n llm_call_started_at = datetime.utcnow()\n analysis = await llm_client.analyze_dashboard(\n screenshot_path,\n logs,\n prompt_template=dashboard_prompt,\n )\n llm_call_finished_at = datetime.utcnow()\n\n # Log analysis summary to task logs for better visibility\n llm_log.info(f\"[ANALYSIS_SUMMARY] Status: {analysis['status']}\")\n llm_log.info(f\"[ANALYSIS_SUMMARY] Summary: {analysis['summary']}\")\n if analysis.get(\"issues\"):\n for i, issue in enumerate(analysis[\"issues\"]):\n llm_log.info(f\"[ANALYSIS_ISSUE][{i+1}] {issue.get('severity')}: {issue.get('message')} (Location: {issue.get('location', 'N/A')})\")\n\n # 6. Persist Result\n validation_result = ValidationResult(\n dashboard_id=dashboard_id,\n status=ValidationStatus(analysis[\"status\"]),\n summary=analysis[\"summary\"],\n issues=[DetectedIssue(**issue) for issue in analysis[\"issues\"]],\n screenshot_path=screenshot_path,\n raw_response=str(analysis)\n )\n validation_finished_at = datetime.utcnow()\n\n result_payload = _json_safe_value(validation_result.dict())\n result_payload[\"screenshot_paths\"] = [screenshot_path]\n result_payload[\"logs_sent_to_llm\"] = logs\n result_payload[\"logs_sent_count\"] = len(logs)\n result_payload[\"prompt_template\"] = dashboard_prompt\n result_payload[\"provider\"] = {\n \"id\": db_provider.id,\n \"name\": db_provider.name,\n \"type\": db_provider.provider_type,\n \"base_url\": db_provider.base_url,\n \"model\": db_provider.default_model,\n }\n result_payload[\"environment_id\"] = env_id\n result_payload[\"timings\"] = {\n \"validation_started_at\": validation_started_at.isoformat(),\n \"validation_finished_at\": validation_finished_at.isoformat(),\n \"validation_duration_ms\": int((validation_finished_at - validation_started_at).total_seconds() * 1000),\n \"screenshot_started_at\": screenshot_started_at.isoformat(),\n \"screenshot_finished_at\": screenshot_finished_at.isoformat(),\n \"screenshot_duration_ms\": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),\n \"logs_fetch_started_at\": logs_fetch_started_at.isoformat(),\n \"logs_fetch_finished_at\": logs_fetch_finished_at.isoformat(),\n \"logs_fetch_duration_ms\": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),\n \"llm_call_started_at\": llm_call_started_at.isoformat(),\n \"llm_call_finished_at\": llm_call_finished_at.isoformat(),\n \"llm_call_duration_ms\": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),\n }\n\n db_record = ValidationRecord(\n task_id=context.task_id if context else None,\n policy_id=params.get(\"policy_id\"),\n dashboard_id=validation_result.dashboard_id,\n environment_id=env_id,\n status=validation_result.status.value,\n summary=validation_result.summary,\n issues=[issue.dict() for issue in validation_result.issues],\n screenshot_path=validation_result.screenshot_path,\n raw_response=json.dumps(result_payload, ensure_ascii=False)\n )\n db.add(db_record)\n db.commit()\n\n # 7. Notification on failure (US1 / FR-015)\n try:\n policy_id = params.get(\"policy_id\")\n policy = None\n if policy_id:\n policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == policy_id).first()\n\n notification_service = NotificationService(db, config_mgr)\n await notification_service.dispatch_report(\n record=db_record,\n policy=policy,\n background_tasks=getattr(context, \"background_tasks\", None) if context else None\n )\n except Exception as e:\n log.error(f\"Failed to dispatch notifications: {e}\")\n\n # Final log to ensure all analysis is visible in task logs\n log.info(f\"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}\")\n\n return result_payload\n\n finally:\n db.close()\n # endregion DashboardValidationPlugin.execute\n# #endregion DashboardValidationPlugin\n\n# #region DocumentationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dataset documentation using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DocumentationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_documentation\"\n\n @property\n def name(self) -> str:\n return \"Dataset LLM Documentation\"\n\n @property\n def description(self) -> str:\n return \"Automated dataset and column documentation 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 \"dataset_id\": {\"type\": \"string\", \"title\": \"Dataset ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dataset_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DocumentationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dataset documentation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Documentation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dataset_id, environment_id, and provider_id.\n # @POST Returns generated documentation and updates the dataset in Superset.\n # @SIDE_EFFECT Calls LLM API and updates dataset metadata in Superset.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\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 llm_log = log.with_source(\"llm\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dataset_id = params.get(\"dataset_id\")\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Fetch Metadata (US2 / T024)\n from ...core.superset_client import SupersetClient\n client = SupersetClient(env)\n\n superset_log.debug(f\"Fetching dataset {dataset_id}\")\n dataset = client.get_dataset(int(dataset_id))\n\n # Extract columns and existing descriptions\n columns_data = []\n for col in dataset.get(\"columns\", []):\n columns_data.append({\n \"name\": col.get(\"column_name\"),\n \"type\": col.get(\"type\"),\n \"description\": col.get(\"description\")\n })\n superset_log.debug(f\"Extracted {len(columns_data)} columns from dataset\")\n\n # 4. Construct Prompt & Analyze (US2 / T025)\n llm_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 llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n documentation_prompt = llm_settings[\"prompts\"].get(\n \"documentation_prompt\",\n DEFAULT_LLM_PROMPTS[\"documentation_prompt\"],\n )\n prompt = render_prompt(\n documentation_prompt,\n {\n \"dataset_name\": dataset.get(\"table_name\") or \"\",\n \"columns_json\": json.dumps(columns_data, ensure_ascii=False),\n },\n )\n\n # Using a generic chat completion for text-only US2\n llm_log.info(f\"Generating documentation for dataset {dataset_id}\")\n doc_result = await llm_client.get_json_completion([{\"role\": \"user\", \"content\": prompt}])\n\n # 5. Update Metadata (US2 / T026)\n update_payload = {\n \"description\": doc_result[\"dataset_description\"],\n \"columns\": []\n }\n\n # Map generated descriptions back to column IDs\n for col_doc in doc_result[\"column_descriptions\"]:\n for col in dataset.get(\"columns\", []):\n if col.get(\"column_name\") == col_doc[\"name\"]:\n update_payload[\"columns\"].append({\n \"id\": col.get(\"id\"),\n \"description\": col_doc[\"description\"]\n })\n\n superset_log.info(f\"Updating dataset {dataset_id} with generated documentation\")\n client.update_dataset(int(dataset_id), update_payload)\n\n log.info(f\"Documentation completed for dataset {dataset_id}\")\n\n return doc_result\n\n finally:\n db.close()\n # endregion DocumentationPlugin.execute\n# #endregion DocumentationPlugin\n\n# #endregion LLMAnalysisPlugin\n" + "body": "# #region LLMAnalysisPlugin [C:5] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]\n# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.\n# @LAYER Plugin\n# @RELATION INHERITS -> [PluginBase]\n# @RELATION CALLS -> [ScreenshotService]\n# @RELATION CALLS -> [LLMClient]\n# @RELATION CALLS -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TaskContext]\n# @INVARIANT All LLM interactions must be executed as asynchronous tasks.\n# @DATA_CONTRACT AnalysisRequest -> AnalysisResult\n\nfrom datetime import UTC, datetime, timedelta\nimport json\nimport os\nfrom typing import Any\n\nfrom ...core.database import SessionLocal\nfrom ...core.logger import belief_scope, logger\nfrom ...core.plugin_base import PluginBase\nfrom ...core.superset_client import SupersetClient\nfrom ...core.task_manager.context import TaskContext\nfrom ...models.llm import ValidationPolicy, ValidationRecord\nfrom ...services.llm_prompt_templates import (\n DEFAULT_LLM_PROMPTS,\n normalize_llm_settings,\n render_prompt,\n)\nfrom ...services.llm_provider import LLMProviderService\nfrom ...services.notifications.service import NotificationService\nfrom .models import DetectedIssue, LLMProviderType, ValidationResult, ValidationStatus\nfrom .service import LLMClient, ScreenshotService\n\n\n# #region _is_masked_or_invalid_api_key [TYPE Function]\n# @BRIEF Guards against placeholder or malformed API keys in runtime.\n# @PRE value may be None.\n# @POST Returns True when value cannot be used for authenticated provider calls.\ndef _is_masked_or_invalid_api_key(value: str | None) -> bool:\n key = (value or \"\").strip()\n if not key:\n return True\n if key in {\"********\", \"EMPTY_OR_NONE\"}:\n return True\n # Most provider tokens are significantly longer; short values are almost always placeholders.\n return len(key) < 16\n# #endregion _is_masked_or_invalid_api_key\n\n# #region _json_safe_value [TYPE Function]\n# @BRIEF Recursively normalize payload values for JSON serialization.\n# @PRE value may be nested dict/list with datetime values.\n# @POST datetime values are converted to ISO strings.\ndef _json_safe_value(value: Any):\n if isinstance(value, datetime):\n return value.isoformat()\n if isinstance(value, dict):\n return {k: _json_safe_value(v) for k, v in value.items()}\n if isinstance(value, list):\n return [_json_safe_value(v) for v in value]\n return value\n# #endregion _json_safe_value\n\n# #region DashboardValidationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dashboard health analysis using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DashboardValidationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_dashboard_validation\"\n\n @property\n def name(self) -> str:\n return \"Dashboard LLM Validation\"\n\n @property\n def description(self) -> str:\n return \"Automated dashboard health analysis using multimodal 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 \"dashboard_id\": {\"type\": \"string\", \"title\": \"Dashboard ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dashboard_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DashboardValidationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dashboard validation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Validation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dashboard_id, environment_id, and provider_id.\n # @POST Returns a dictionary with validation results and persists them to the database.\n # @SIDE_EFFECT Captures a screenshot, calls LLM API, and writes to the database.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\n validation_started_at = datetime.now(UTC)\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 llm_log = log.with_source(\"llm\") if context else log\n screenshot_log = log.with_source(\"screenshot\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dashboard_id_raw = params.get(\"dashboard_id\")\n dashboard_id = str(dashboard_id_raw) if dashboard_id_raw is not None else None\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n llm_log.debug(f\" Is Active: {db_provider.is_active}\")\n llm_log.debug(f\" Is Multimodal: {db_provider.is_multimodal}\")\n if not bool(db_provider.is_multimodal):\n raise ValueError(\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Capture Screenshot\n screenshot_service = ScreenshotService(env)\n\n storage_root = config_mgr.get_config().settings.storage.root_path\n screenshots_dir = os.path.join(storage_root, \"screenshots\")\n os.makedirs(screenshots_dir, exist_ok=True)\n\n filename = f\"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png\"\n screenshot_path = os.path.join(screenshots_dir, filename)\n\n screenshot_started_at = datetime.now(UTC)\n screenshot_log.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)\n screenshot_log.debug(f\"Screenshot saved to: {screenshot_path}\")\n screenshot_finished_at = datetime.now(UTC)\n\n # 4. Fetch Logs (from Environment /api/v1/log/)\n logs = []\n logs_fetch_started_at = datetime.now(UTC)\n try:\n client = SupersetClient(env)\n\n # Calculate time window (last 24 hours)\n start_time = (datetime.now() - timedelta(hours=24)).isoformat()\n\n # Construct filter for logs\n # Note: We filter by dashboard_id matching the object\n query_params = {\n \"filters\": [\n {\"col\": \"dashboard_id\", \"opr\": \"eq\", \"value\": dashboard_id},\n {\"col\": \"dttm\", \"opr\": \"gt\", \"value\": start_time}\n ],\n \"order_column\": \"dttm\",\n \"order_direction\": \"desc\",\n \"page\": 0,\n \"page_size\": 100\n }\n\n superset_log.debug(f\"Fetching logs for dashboard {dashboard_id}\")\n response = client.network.request(\n method=\"GET\",\n endpoint=\"/log/\",\n params={\"q\": json.dumps(query_params)}\n )\n\n if isinstance(response, dict) and \"result\" in response:\n for item in response[\"result\"]:\n action = item.get(\"action\", \"unknown\")\n dttm = item.get(\"dttm\", \"\")\n details = item.get(\"json\", \"\")\n logs.append(f\"[{dttm}] {action}: {details}\")\n\n if not logs:\n logs = [\"No recent logs found for this dashboard.\"]\n superset_log.debug(\"No recent logs found for this dashboard\")\n\n except Exception as e:\n superset_log.warning(f\"Failed to fetch logs from environment: {e}\")\n logs = [f\"Error fetching remote logs: {e!s}\"]\n logs_fetch_finished_at = datetime.now(UTC)\n\n # 5. Analyze with LLM\n llm_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 llm_log.info(f\"Analyzing dashboard {dashboard_id} with LLM\")\n llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n dashboard_prompt = llm_settings[\"prompts\"].get(\n \"dashboard_validation_prompt\",\n DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n )\n llm_call_started_at = datetime.now(UTC)\n analysis = await llm_client.analyze_dashboard(\n screenshot_path,\n logs,\n prompt_template=dashboard_prompt,\n )\n llm_call_finished_at = datetime.now(UTC)\n\n # Log analysis summary to task logs for better visibility\n llm_log.info(f\"[ANALYSIS_SUMMARY] Status: {analysis['status']}\")\n llm_log.info(f\"[ANALYSIS_SUMMARY] Summary: {analysis['summary']}\")\n if analysis.get(\"issues\"):\n for i, issue in enumerate(analysis[\"issues\"]):\n llm_log.info(f\"[ANALYSIS_ISSUE][{i+1}] {issue.get('severity')}: {issue.get('message')} (Location: {issue.get('location', 'N/A')})\")\n\n # 6. Persist Result\n validation_result = ValidationResult(\n dashboard_id=dashboard_id,\n status=ValidationStatus(analysis[\"status\"]),\n summary=analysis[\"summary\"],\n issues=[DetectedIssue(**issue) for issue in analysis[\"issues\"]],\n screenshot_path=screenshot_path,\n raw_response=str(analysis)\n )\n validation_finished_at = datetime.now(UTC)\n\n result_payload = _json_safe_value(validation_result.model_dump())\n result_payload[\"screenshot_paths\"] = [screenshot_path]\n result_payload[\"logs_sent_to_llm\"] = logs\n result_payload[\"logs_sent_count\"] = len(logs)\n result_payload[\"prompt_template\"] = dashboard_prompt\n result_payload[\"provider\"] = {\n \"id\": db_provider.id,\n \"name\": db_provider.name,\n \"type\": db_provider.provider_type,\n \"base_url\": db_provider.base_url,\n \"model\": db_provider.default_model,\n }\n result_payload[\"environment_id\"] = env_id\n result_payload[\"timings\"] = {\n \"validation_started_at\": validation_started_at.isoformat(),\n \"validation_finished_at\": validation_finished_at.isoformat(),\n \"validation_duration_ms\": int((validation_finished_at - validation_started_at).total_seconds() * 1000),\n \"screenshot_started_at\": screenshot_started_at.isoformat(),\n \"screenshot_finished_at\": screenshot_finished_at.isoformat(),\n \"screenshot_duration_ms\": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),\n \"logs_fetch_started_at\": logs_fetch_started_at.isoformat(),\n \"logs_fetch_finished_at\": logs_fetch_finished_at.isoformat(),\n \"logs_fetch_duration_ms\": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),\n \"llm_call_started_at\": llm_call_started_at.isoformat(),\n \"llm_call_finished_at\": llm_call_finished_at.isoformat(),\n \"llm_call_duration_ms\": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),\n }\n\n db_record = ValidationRecord(\n task_id=context.task_id if context else None,\n policy_id=params.get(\"policy_id\"),\n dashboard_id=validation_result.dashboard_id,\n environment_id=env_id,\n status=validation_result.status.value,\n summary=validation_result.summary,\n issues=[issue.model_dump() for issue in validation_result.issues],\n screenshot_path=validation_result.screenshot_path,\n raw_response=json.dumps(result_payload, ensure_ascii=False)\n )\n db.add(db_record)\n db.commit()\n\n # 7. Notification on failure (US1 / FR-015)\n try:\n policy_id = params.get(\"policy_id\")\n policy = None\n if policy_id:\n policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == policy_id).first()\n\n notification_service = NotificationService(db, config_mgr)\n await notification_service.dispatch_report(\n record=db_record,\n policy=policy,\n background_tasks=getattr(context, \"background_tasks\", None) if context else None\n )\n except Exception as e:\n log.error(f\"Failed to dispatch notifications: {e}\")\n\n # Final log to ensure all analysis is visible in task logs\n log.info(f\"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}\")\n\n return result_payload\n\n finally:\n db.close()\n # endregion DashboardValidationPlugin.execute\n# #endregion DashboardValidationPlugin\n\n# #region DocumentationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dataset documentation using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DocumentationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_documentation\"\n\n @property\n def name(self) -> str:\n return \"Dataset LLM Documentation\"\n\n @property\n def description(self) -> str:\n return \"Automated dataset and column documentation 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 \"dataset_id\": {\"type\": \"string\", \"title\": \"Dataset ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dataset_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DocumentationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dataset documentation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Documentation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dataset_id, environment_id, and provider_id.\n # @POST Returns generated documentation and updates the dataset in Superset.\n # @SIDE_EFFECT Calls LLM API and updates dataset metadata in Superset.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\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 llm_log = log.with_source(\"llm\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dataset_id = params.get(\"dataset_id\")\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Fetch Metadata (US2 / T024)\n from ...core.superset_client import SupersetClient\n client = SupersetClient(env)\n\n superset_log.debug(f\"Fetching dataset {dataset_id}\")\n dataset = client.get_dataset(int(dataset_id))\n\n # Extract columns and existing descriptions\n columns_data = []\n for col in dataset.get(\"columns\", []):\n columns_data.append({\n \"name\": col.get(\"column_name\"),\n \"type\": col.get(\"type\"),\n \"description\": col.get(\"description\")\n })\n superset_log.debug(f\"Extracted {len(columns_data)} columns from dataset\")\n\n # 4. Construct Prompt & Analyze (US2 / T025)\n llm_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 llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n documentation_prompt = llm_settings[\"prompts\"].get(\n \"documentation_prompt\",\n DEFAULT_LLM_PROMPTS[\"documentation_prompt\"],\n )\n prompt = render_prompt(\n documentation_prompt,\n {\n \"dataset_name\": dataset.get(\"table_name\") or \"\",\n \"columns_json\": json.dumps(columns_data, ensure_ascii=False),\n },\n )\n\n # Using a generic chat completion for text-only US2\n llm_log.info(f\"Generating documentation for dataset {dataset_id}\")\n doc_result = await llm_client.get_json_completion([{\"role\": \"user\", \"content\": prompt}])\n\n # 5. Update Metadata (US2 / T026)\n update_payload = {\n \"description\": doc_result[\"dataset_description\"],\n \"columns\": []\n }\n\n # Map generated descriptions back to column IDs\n for col_doc in doc_result[\"column_descriptions\"]:\n for col in dataset.get(\"columns\", []):\n if col.get(\"column_name\") == col_doc[\"name\"]:\n update_payload[\"columns\"].append({\n \"id\": col.get(\"id\"),\n \"description\": col_doc[\"description\"]\n })\n\n superset_log.info(f\"Updating dataset {dataset_id} with generated documentation\")\n client.update_dataset(int(dataset_id), update_payload)\n\n log.info(f\"Documentation completed for dataset {dataset_id}\")\n\n return doc_result\n\n finally:\n db.close()\n # endregion DocumentationPlugin.execute\n# #endregion DocumentationPlugin\n\n# #endregion LLMAnalysisPlugin\n" }, { "contract_id": "_is_masked_or_invalid_api_key", @@ -35745,7 +36618,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DashboardValidationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dashboard health analysis using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DashboardValidationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_dashboard_validation\"\n\n @property\n def name(self) -> str:\n return \"Dashboard LLM Validation\"\n\n @property\n def description(self) -> str:\n return \"Automated dashboard health analysis using multimodal 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 \"dashboard_id\": {\"type\": \"string\", \"title\": \"Dashboard ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dashboard_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DashboardValidationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dashboard validation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Validation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dashboard_id, environment_id, and provider_id.\n # @POST Returns a dictionary with validation results and persists them to the database.\n # @SIDE_EFFECT Captures a screenshot, calls LLM API, and writes to the database.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\n validation_started_at = datetime.utcnow()\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 llm_log = log.with_source(\"llm\") if context else log\n screenshot_log = log.with_source(\"screenshot\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dashboard_id_raw = params.get(\"dashboard_id\")\n dashboard_id = str(dashboard_id_raw) if dashboard_id_raw is not None else None\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n llm_log.debug(f\" Is Active: {db_provider.is_active}\")\n llm_log.debug(f\" Is Multimodal: {db_provider.is_multimodal}\")\n if not bool(db_provider.is_multimodal):\n raise ValueError(\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Capture Screenshot\n screenshot_service = ScreenshotService(env)\n\n storage_root = config_mgr.get_config().settings.storage.root_path\n screenshots_dir = os.path.join(storage_root, \"screenshots\")\n os.makedirs(screenshots_dir, exist_ok=True)\n\n filename = f\"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png\"\n screenshot_path = os.path.join(screenshots_dir, filename)\n\n screenshot_started_at = datetime.utcnow()\n screenshot_log.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)\n screenshot_log.debug(f\"Screenshot saved to: {screenshot_path}\")\n screenshot_finished_at = datetime.utcnow()\n\n # 4. Fetch Logs (from Environment /api/v1/log/)\n logs = []\n logs_fetch_started_at = datetime.utcnow()\n try:\n client = SupersetClient(env)\n\n # Calculate time window (last 24 hours)\n start_time = (datetime.now() - timedelta(hours=24)).isoformat()\n\n # Construct filter for logs\n # Note: We filter by dashboard_id matching the object\n query_params = {\n \"filters\": [\n {\"col\": \"dashboard_id\", \"opr\": \"eq\", \"value\": dashboard_id},\n {\"col\": \"dttm\", \"opr\": \"gt\", \"value\": start_time}\n ],\n \"order_column\": \"dttm\",\n \"order_direction\": \"desc\",\n \"page\": 0,\n \"page_size\": 100\n }\n\n superset_log.debug(f\"Fetching logs for dashboard {dashboard_id}\")\n response = client.network.request(\n method=\"GET\",\n endpoint=\"/log/\",\n params={\"q\": json.dumps(query_params)}\n )\n\n if isinstance(response, dict) and \"result\" in response:\n for item in response[\"result\"]:\n action = item.get(\"action\", \"unknown\")\n dttm = item.get(\"dttm\", \"\")\n details = item.get(\"json\", \"\")\n logs.append(f\"[{dttm}] {action}: {details}\")\n\n if not logs:\n logs = [\"No recent logs found for this dashboard.\"]\n superset_log.debug(\"No recent logs found for this dashboard\")\n\n except Exception as e:\n superset_log.warning(f\"Failed to fetch logs from environment: {e}\")\n logs = [f\"Error fetching remote logs: {e!s}\"]\n logs_fetch_finished_at = datetime.utcnow()\n\n # 5. Analyze with LLM\n llm_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 llm_log.info(f\"Analyzing dashboard {dashboard_id} with LLM\")\n llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n dashboard_prompt = llm_settings[\"prompts\"].get(\n \"dashboard_validation_prompt\",\n DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n )\n llm_call_started_at = datetime.utcnow()\n analysis = await llm_client.analyze_dashboard(\n screenshot_path,\n logs,\n prompt_template=dashboard_prompt,\n )\n llm_call_finished_at = datetime.utcnow()\n\n # Log analysis summary to task logs for better visibility\n llm_log.info(f\"[ANALYSIS_SUMMARY] Status: {analysis['status']}\")\n llm_log.info(f\"[ANALYSIS_SUMMARY] Summary: {analysis['summary']}\")\n if analysis.get(\"issues\"):\n for i, issue in enumerate(analysis[\"issues\"]):\n llm_log.info(f\"[ANALYSIS_ISSUE][{i+1}] {issue.get('severity')}: {issue.get('message')} (Location: {issue.get('location', 'N/A')})\")\n\n # 6. Persist Result\n validation_result = ValidationResult(\n dashboard_id=dashboard_id,\n status=ValidationStatus(analysis[\"status\"]),\n summary=analysis[\"summary\"],\n issues=[DetectedIssue(**issue) for issue in analysis[\"issues\"]],\n screenshot_path=screenshot_path,\n raw_response=str(analysis)\n )\n validation_finished_at = datetime.utcnow()\n\n result_payload = _json_safe_value(validation_result.dict())\n result_payload[\"screenshot_paths\"] = [screenshot_path]\n result_payload[\"logs_sent_to_llm\"] = logs\n result_payload[\"logs_sent_count\"] = len(logs)\n result_payload[\"prompt_template\"] = dashboard_prompt\n result_payload[\"provider\"] = {\n \"id\": db_provider.id,\n \"name\": db_provider.name,\n \"type\": db_provider.provider_type,\n \"base_url\": db_provider.base_url,\n \"model\": db_provider.default_model,\n }\n result_payload[\"environment_id\"] = env_id\n result_payload[\"timings\"] = {\n \"validation_started_at\": validation_started_at.isoformat(),\n \"validation_finished_at\": validation_finished_at.isoformat(),\n \"validation_duration_ms\": int((validation_finished_at - validation_started_at).total_seconds() * 1000),\n \"screenshot_started_at\": screenshot_started_at.isoformat(),\n \"screenshot_finished_at\": screenshot_finished_at.isoformat(),\n \"screenshot_duration_ms\": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),\n \"logs_fetch_started_at\": logs_fetch_started_at.isoformat(),\n \"logs_fetch_finished_at\": logs_fetch_finished_at.isoformat(),\n \"logs_fetch_duration_ms\": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),\n \"llm_call_started_at\": llm_call_started_at.isoformat(),\n \"llm_call_finished_at\": llm_call_finished_at.isoformat(),\n \"llm_call_duration_ms\": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),\n }\n\n db_record = ValidationRecord(\n task_id=context.task_id if context else None,\n policy_id=params.get(\"policy_id\"),\n dashboard_id=validation_result.dashboard_id,\n environment_id=env_id,\n status=validation_result.status.value,\n summary=validation_result.summary,\n issues=[issue.dict() for issue in validation_result.issues],\n screenshot_path=validation_result.screenshot_path,\n raw_response=json.dumps(result_payload, ensure_ascii=False)\n )\n db.add(db_record)\n db.commit()\n\n # 7. Notification on failure (US1 / FR-015)\n try:\n policy_id = params.get(\"policy_id\")\n policy = None\n if policy_id:\n policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == policy_id).first()\n\n notification_service = NotificationService(db, config_mgr)\n await notification_service.dispatch_report(\n record=db_record,\n policy=policy,\n background_tasks=getattr(context, \"background_tasks\", None) if context else None\n )\n except Exception as e:\n log.error(f\"Failed to dispatch notifications: {e}\")\n\n # Final log to ensure all analysis is visible in task logs\n log.info(f\"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}\")\n\n return result_payload\n\n finally:\n db.close()\n # endregion DashboardValidationPlugin.execute\n# #endregion DashboardValidationPlugin\n" + "body": "# #region DashboardValidationPlugin [TYPE Class]\n# @BRIEF Plugin for automated dashboard health analysis using LLMs.\n# @RELATION IMPLEMENTS -> [PluginBase]\nclass DashboardValidationPlugin(PluginBase):\n @property\n def id(self) -> str:\n return \"llm_dashboard_validation\"\n\n @property\n def name(self) -> str:\n return \"Dashboard LLM Validation\"\n\n @property\n def description(self) -> str:\n return \"Automated dashboard health analysis using multimodal 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 \"dashboard_id\": {\"type\": \"string\", \"title\": \"Dashboard ID\"},\n \"environment_id\": {\"type\": \"string\", \"title\": \"Environment ID\"},\n \"provider_id\": {\"type\": \"string\", \"title\": \"LLM Provider ID\"}\n },\n \"required\": [\"dashboard_id\", \"environment_id\", \"provider_id\"]\n }\n\n # region DashboardValidationPlugin.execute [TYPE Function]\n # @PURPOSE: Executes the dashboard validation task with TaskContext support.\n # @PARAM params (Dict[str, Any]) - Validation parameters.\n # @PARAM context (Optional[TaskContext]) - Task context for logging with source attribution.\n # @PRE params contains dashboard_id, environment_id, and provider_id.\n # @POST Returns a dictionary with validation results and persists them to the database.\n # @SIDE_EFFECT Captures a screenshot, calls LLM API, and writes to the database.\n async def execute(self, params: dict[str, Any], context: TaskContext | None = None):\n with belief_scope(\"execute\", f\"plugin_id={self.id}\"):\n validation_started_at = datetime.now(UTC)\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 llm_log = log.with_source(\"llm\") if context else log\n screenshot_log = log.with_source(\"screenshot\") if context else log\n superset_log = log.with_source(\"superset_api\") if context else log\n\n log.info(f\"Executing {self.name} with params: {params}\")\n\n dashboard_id_raw = params.get(\"dashboard_id\")\n dashboard_id = str(dashboard_id_raw) if dashboard_id_raw is not None else None\n env_id = params.get(\"environment_id\")\n provider_id = params.get(\"provider_id\")\n\n db = SessionLocal()\n try:\n # 1. Get Environment\n from ...dependencies import get_config_manager\n config_mgr = get_config_manager()\n env = config_mgr.get_environment(env_id)\n if not env:\n log.error(f\"Environment {env_id} not found\")\n raise ValueError(f\"Environment {env_id} not found\")\n\n # 2. Get LLM Provider\n llm_service = LLMProviderService(db)\n db_provider = llm_service.get_provider(provider_id)\n if not db_provider:\n log.error(f\"LLM Provider {provider_id} not found\")\n raise ValueError(f\"LLM Provider {provider_id} not found\")\n\n llm_log.debug(\"Retrieved provider config:\")\n llm_log.debug(f\" Provider ID: {db_provider.id}\")\n llm_log.debug(f\" Provider Name: {db_provider.name}\")\n llm_log.debug(f\" Provider Type: {db_provider.provider_type}\")\n llm_log.debug(f\" Base URL: {db_provider.base_url}\")\n llm_log.debug(f\" Default Model: {db_provider.default_model}\")\n llm_log.debug(f\" Is Active: {db_provider.is_active}\")\n llm_log.debug(f\" Is Multimodal: {db_provider.is_multimodal}\")\n if not bool(db_provider.is_multimodal):\n raise ValueError(\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n\n api_key = llm_service.get_decrypted_api_key(provider_id)\n llm_log.debug(f\"API Key decrypted (first 8 chars): {api_key[:8] if api_key and len(api_key) > 8 else 'EMPTY_OR_NONE'}...\")\n\n # Check if API key was successfully decrypted\n if _is_masked_or_invalid_api_key(api_key):\n raise ValueError(\n f\"Invalid API key for provider {provider_id}. \"\n \"Please open LLM provider settings and save a real API key (not masked placeholder).\"\n )\n\n # 3. Capture Screenshot\n screenshot_service = ScreenshotService(env)\n\n storage_root = config_mgr.get_config().settings.storage.root_path\n screenshots_dir = os.path.join(storage_root, \"screenshots\")\n os.makedirs(screenshots_dir, exist_ok=True)\n\n filename = f\"{dashboard_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png\"\n screenshot_path = os.path.join(screenshots_dir, filename)\n\n screenshot_started_at = datetime.now(UTC)\n screenshot_log.info(f\"Capturing screenshot for dashboard {dashboard_id}\")\n await screenshot_service.capture_dashboard(dashboard_id, screenshot_path)\n screenshot_log.debug(f\"Screenshot saved to: {screenshot_path}\")\n screenshot_finished_at = datetime.now(UTC)\n\n # 4. Fetch Logs (from Environment /api/v1/log/)\n logs = []\n logs_fetch_started_at = datetime.now(UTC)\n try:\n client = SupersetClient(env)\n\n # Calculate time window (last 24 hours)\n start_time = (datetime.now() - timedelta(hours=24)).isoformat()\n\n # Construct filter for logs\n # Note: We filter by dashboard_id matching the object\n query_params = {\n \"filters\": [\n {\"col\": \"dashboard_id\", \"opr\": \"eq\", \"value\": dashboard_id},\n {\"col\": \"dttm\", \"opr\": \"gt\", \"value\": start_time}\n ],\n \"order_column\": \"dttm\",\n \"order_direction\": \"desc\",\n \"page\": 0,\n \"page_size\": 100\n }\n\n superset_log.debug(f\"Fetching logs for dashboard {dashboard_id}\")\n response = client.network.request(\n method=\"GET\",\n endpoint=\"/log/\",\n params={\"q\": json.dumps(query_params)}\n )\n\n if isinstance(response, dict) and \"result\" in response:\n for item in response[\"result\"]:\n action = item.get(\"action\", \"unknown\")\n dttm = item.get(\"dttm\", \"\")\n details = item.get(\"json\", \"\")\n logs.append(f\"[{dttm}] {action}: {details}\")\n\n if not logs:\n logs = [\"No recent logs found for this dashboard.\"]\n superset_log.debug(\"No recent logs found for this dashboard\")\n\n except Exception as e:\n superset_log.warning(f\"Failed to fetch logs from environment: {e}\")\n logs = [f\"Error fetching remote logs: {e!s}\"]\n logs_fetch_finished_at = datetime.now(UTC)\n\n # 5. Analyze with LLM\n llm_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 llm_log.info(f\"Analyzing dashboard {dashboard_id} with LLM\")\n llm_settings = normalize_llm_settings(config_mgr.get_config().settings.llm)\n dashboard_prompt = llm_settings[\"prompts\"].get(\n \"dashboard_validation_prompt\",\n DEFAULT_LLM_PROMPTS[\"dashboard_validation_prompt\"],\n )\n llm_call_started_at = datetime.now(UTC)\n analysis = await llm_client.analyze_dashboard(\n screenshot_path,\n logs,\n prompt_template=dashboard_prompt,\n )\n llm_call_finished_at = datetime.now(UTC)\n\n # Log analysis summary to task logs for better visibility\n llm_log.info(f\"[ANALYSIS_SUMMARY] Status: {analysis['status']}\")\n llm_log.info(f\"[ANALYSIS_SUMMARY] Summary: {analysis['summary']}\")\n if analysis.get(\"issues\"):\n for i, issue in enumerate(analysis[\"issues\"]):\n llm_log.info(f\"[ANALYSIS_ISSUE][{i+1}] {issue.get('severity')}: {issue.get('message')} (Location: {issue.get('location', 'N/A')})\")\n\n # 6. Persist Result\n validation_result = ValidationResult(\n dashboard_id=dashboard_id,\n status=ValidationStatus(analysis[\"status\"]),\n summary=analysis[\"summary\"],\n issues=[DetectedIssue(**issue) for issue in analysis[\"issues\"]],\n screenshot_path=screenshot_path,\n raw_response=str(analysis)\n )\n validation_finished_at = datetime.now(UTC)\n\n result_payload = _json_safe_value(validation_result.model_dump())\n result_payload[\"screenshot_paths\"] = [screenshot_path]\n result_payload[\"logs_sent_to_llm\"] = logs\n result_payload[\"logs_sent_count\"] = len(logs)\n result_payload[\"prompt_template\"] = dashboard_prompt\n result_payload[\"provider\"] = {\n \"id\": db_provider.id,\n \"name\": db_provider.name,\n \"type\": db_provider.provider_type,\n \"base_url\": db_provider.base_url,\n \"model\": db_provider.default_model,\n }\n result_payload[\"environment_id\"] = env_id\n result_payload[\"timings\"] = {\n \"validation_started_at\": validation_started_at.isoformat(),\n \"validation_finished_at\": validation_finished_at.isoformat(),\n \"validation_duration_ms\": int((validation_finished_at - validation_started_at).total_seconds() * 1000),\n \"screenshot_started_at\": screenshot_started_at.isoformat(),\n \"screenshot_finished_at\": screenshot_finished_at.isoformat(),\n \"screenshot_duration_ms\": int((screenshot_finished_at - screenshot_started_at).total_seconds() * 1000),\n \"logs_fetch_started_at\": logs_fetch_started_at.isoformat(),\n \"logs_fetch_finished_at\": logs_fetch_finished_at.isoformat(),\n \"logs_fetch_duration_ms\": int((logs_fetch_finished_at - logs_fetch_started_at).total_seconds() * 1000),\n \"llm_call_started_at\": llm_call_started_at.isoformat(),\n \"llm_call_finished_at\": llm_call_finished_at.isoformat(),\n \"llm_call_duration_ms\": int((llm_call_finished_at - llm_call_started_at).total_seconds() * 1000),\n }\n\n db_record = ValidationRecord(\n task_id=context.task_id if context else None,\n policy_id=params.get(\"policy_id\"),\n dashboard_id=validation_result.dashboard_id,\n environment_id=env_id,\n status=validation_result.status.value,\n summary=validation_result.summary,\n issues=[issue.model_dump() for issue in validation_result.issues],\n screenshot_path=validation_result.screenshot_path,\n raw_response=json.dumps(result_payload, ensure_ascii=False)\n )\n db.add(db_record)\n db.commit()\n\n # 7. Notification on failure (US1 / FR-015)\n try:\n policy_id = params.get(\"policy_id\")\n policy = None\n if policy_id:\n policy = db.query(ValidationPolicy).filter(ValidationPolicy.id == policy_id).first()\n\n notification_service = NotificationService(db, config_mgr)\n await notification_service.dispatch_report(\n record=db_record,\n policy=policy,\n background_tasks=getattr(context, \"background_tasks\", None) if context else None\n )\n except Exception as e:\n log.error(f\"Failed to dispatch notifications: {e}\")\n\n # Final log to ensure all analysis is visible in task logs\n log.info(f\"Validation completed for dashboard {dashboard_id}. Status: {validation_result.status.value}\")\n\n return result_payload\n\n finally:\n db.close()\n # endregion DashboardValidationPlugin.execute\n# #endregion DashboardValidationPlugin\n" }, { "contract_id": "DocumentationPlugin", @@ -35837,7 +36710,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/llm_analysis/service.py", "start_line": 1, - "end_line": 1174, + "end_line": 1223, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -35849,18 +36722,6 @@ "RATIONALE": "Extracted all hardcoded timeouts into named module-level constants (PLAYWRIGHT_NAVIGATION_TIMEOUT_MS, PLAYWRIGHT_WAIT_TIMEOUT_MS, PLAYWRIGHT_SHORT_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SCREENSHOT_SERVICE_TIMEOUT_MS, LLM_HTTP_TIMEOUT_S) and DEFAULT_USER_AGENT. Zero remaining numeric timeout literals." }, "relations": [ - { - "source_id": "LLMAnalysisService", - "relation_type": "DEPENDS_ON", - "target_id": "EXT:Library:tenacity", - "target_ref": "[EXT:Library:tenacity]" - }, - { - "source_id": "LLMAnalysisService", - "relation_type": "DEPENDS_ON", - "target_id": "EXT:Library:tenacity", - "target_ref": "[EXT:Library:tenacity]" - }, { "source_id": "LLMAnalysisService", "relation_type": "DEPENDS_ON", @@ -35871,14 +36732,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMAnalysisService [C:5] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]\n# @BRIEF Services for LLM interaction and dashboard screenshots.\n# @LAYER Plugin\n# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]\n# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]\n# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]\n# @INVARIANT Screenshots must be 1920px width and capture full page height.\n# @DATA_CONTRACT DashboardSpec -> Screenshot + Analysis\n# @RATIONALE Extracted all hardcoded timeouts into named module-level constants (PLAYWRIGHT_NAVIGATION_TIMEOUT_MS, PLAYWRIGHT_WAIT_TIMEOUT_MS, PLAYWRIGHT_SHORT_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SCREENSHOT_SERVICE_TIMEOUT_MS, LLM_HTTP_TIMEOUT_S) and DEFAULT_USER_AGENT. Zero remaining numeric timeout literals.\n\nimport asyncio\nimport base64\nimport io\nimport json\nimport os\nimport re\nimport shutil\nimport tempfile\nfrom typing import Any\nfrom urllib.parse import urlsplit\n\nimport httpx\nfrom openai import AsyncOpenAI, AuthenticationError as OpenAIAuthenticationError, RateLimitError\nfrom PIL import Image\nfrom playwright.async_api import async_playwright\nfrom tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential\n\nfrom ...core.config_models import Environment\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\nfrom .models import LLMProviderType\n\n# Timeout constants (milliseconds unless noted)\nPLAYWRIGHT_NAVIGATION_TIMEOUT_MS = 30000\nPLAYWRIGHT_WAIT_TIMEOUT_MS = 10000\nPLAYWRIGHT_SHORT_TIMEOUT_MS = 5000\nHTTP_REQUEST_TIMEOUT_MS = 60000\nSCREENSHOT_SERVICE_TIMEOUT_MS = 120000\nLLM_HTTP_TIMEOUT_S = 120 # seconds (httpx client timeout)\nDEFAULT_USER_AGENT = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36\"\n\n# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE 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 # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE 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 # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE 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 # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE 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 # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE 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 # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE 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 # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE 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 # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE 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=PLAYWRIGHT_WAIT_TIMEOUT_MS,\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 # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE 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 # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE 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 # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE 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 = HTTP_REQUEST_TIMEOUT_MS,\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 # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService._wait_for_charts_stabilized [TYPE Function] [C:2]\n # @BRIEF Wait until chart elements have non-zero dimensions, with polling.\n # @PRE page is a valid Playwright page.\n # @POST Waits for chart stabilization or raises on timeout (handled internally).\n # @RATIONALE Polls for actual chart rendering dimensions rather than using a fixed delay — charts may load at different speeds depending on dashboard complexity and network conditions.\n # @REJECTED Fixed sleep-based wait rejected — would either waste time (too long) or produce blank screenshots (too short); polling for actual canvas/svg dimensions is more reliable.\n async def _wait_for_charts_stabilized(self, page, timeout_ms: int = 15000):\n \"\"\"Wait until chart elements have non-zero dimensions, with polling.\"\"\"\n # Short initial delay for rendering pipeline to start\n await asyncio.sleep(0.5)\n try:\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container canvas, .slice_container svg, .grid-content canvas');\n if (charts.length === 0) return true;\n return Array.from(charts).some(c => {\n if (c.tagName === 'CANVAS') return c.width > 10 && c.height > 10;\n if (c.tagName === 'svg') {\n const bbox = c.getBoundingClientRect();\n return bbox.width > 10 && bbox.height > 10;\n }\n return false;\n });\n }\"\"\", timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Chart stabilization wait timed out, proceeding anyway\")\n # endregion ScreenshotService._wait_for_charts_stabilized\n\n # region ScreenshotService._wait_for_resize_rendered [TYPE Function] [C:2]\n # @BRIEF Wait for charts to re-render after viewport resize.\n # @PRE page is a valid Playwright page; chart_count_before contains pre-resize element counts.\n # @POST Waits for chart content to return or timeout.\n # @RATIONALE After viewport resize, Superset triggers lazy chart re-rendering — this function polls for chart elements to reappear before taking the screenshot.\n # @REJECTED Single fixed wait after resize rejected — some dashboards re-render instantly while others take seconds; fixed wait is brittle across dashboard types.\n async def _wait_for_resize_rendered(self, page, chart_count_before: dict, timeout_ms: int = 10000):\n \"\"\"Wait for charts to re-render after viewport resize, with polling.\"\"\"\n try:\n await page.wait_for_function(\"\"\"(preCounts) => {\n const currentCharts = document.querySelectorAll('.chart-container, .slice_container').length;\n const currentCanvases = document.querySelectorAll('canvas').length;\n const currentSvgs = document.querySelectorAll('.chart-container svg, .slice_container svg').length;\n // At least one chart element must be present\n return currentCharts > 0 && (currentCanvases > 0 || currentSvgs > 0);\n }\"\"\", arg=chart_count_before, timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Re-render wait timed out after viewport resize, proceeding anyway\")\n # endregion ScreenshotService._wait_for_resize_rendered\n\n # region ScreenshotService._save_debug_screenshot [TYPE Function] [C:1]\n # @BRIEF Save a debug screenshot to a temp directory for diagnostic purposes.\n # @PRE debug_dir exists and is writable.\n # @POST Returns the debug path or None on failure.\n # @RATIONALE Uses tempfile.mkdtemp() to avoid accumulating .png files in production storage. Temp dir is cleaned up on success (rmtree) or preserved on failure for debugging.\n # @REJECTED Old approach saved debug .png next to output path — accumulated _debug_failed_login.png, _preresize.png permanently in screenshots dir (F1). In-memory-only debug logging rejected — screenshot state is visual and cannot be captured in logs.\n async def _save_debug_screenshot(self, page, debug_dir: str, suffix: str) -> str | None:\n debug_path = os.path.join(debug_dir, suffix)\n try:\n await page.screenshot(path=debug_path)\n return debug_path\n except Exception:\n return None\n # endregion ScreenshotService._save_debug_screenshot\n\n # region ScreenshotService.capture_dashboard [TYPE Function] [C:4]\n # @PURPOSE Captures a full-page screenshot of a dashboard using Playwright and CDP, plus diagnostic viewport screenshots of tabs that had chart load errors.\n # @PRE dashboard_id is a valid string, output_path is a writable path.\n # @POST Returns True if screenshot is saved successfully. Debug temp dir cleaned up on success, preserved on failure.\n # Extra diagnostic screenshots saved as output_path__tab_{tab_text}.png for each tab with chart errors.\n # @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, writes PNG files; creates and cleans up temp debug directory.\n # @UX_STATE [Navigating] -> Loading dashboard UI\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # @UX_STATE [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE [Capturing] -> Executing CDP screenshot\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # @RATIONALE Errored tab screenshots give LLM diagnostic context — an error text description (\"charts.length = 0\") is not enough to diagnose a chart load failure; a visual screenshot shows whether the tab is empty, has a loading spinner, or renders a Superset error card. Uses tempfile.mkdtemp() for all debug screenshots; cleans up on success (shutil.rmtree), preserves on failure for diagnostic analysis.\n # @REJECTED Always-screenshot-all-tabs rejected — ~20 tabs x 5s per screenshot = 100s overhead, most tabs healthy. Only errored + main gives diagnostic value at near-zero cost. Old approach saved debug .png files alongside output — accumulated permanently (F1). In-memory-only logging rejected — visual state cannot be captured in text.\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n debug_dir = tempfile.mkdtemp(prefix=\"ss_screenshot_debug_\")\n success = False\n try:\n # -- begin original body --\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=HTTP_REQUEST_TIMEOUT_MS,\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\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: {e!s}\")\n debug_path = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\n raise RuntimeError(f\"Login failed: {e!s}. 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=HTTP_REQUEST_TIMEOUT_MS,\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_dashboard_auth.png\")\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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 - wait for charts to stabilize\n logger.info(\"[DEBUG] Waiting for charts to stabilize...\")\n await self._wait_for_charts_stabilized(page)\n\n # Logic to handle tabs and full-page capture\n try:\n async with asyncio.timeout(SCREENSHOT_SERVICE_TIMEOUT_MS / 1000):\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # These are later screenshotted individually for LLM diagnostic value.\n processed_tabs = set()\n errored_tabs = [] # Collects {tab_text, depth, error} for tabs with chart load failures\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 # Wait for chart content to render in the newly active tab\n try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception:\n logger.warning(f\"[TabSwitching] Content verification timed out for tab: {tab_text}\")\n errored_tabs.append({\n \"tab_text\": tab_text,\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": \"content_verification_timeout\"\n })\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 errored_tabs.append({\n \"tab_text\": f\"tab_{depth}_{i}\",\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": f\"processing_exception: {tab_e!s}\"\n })\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 try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n except Exception:\n logger.warning(\"[TabSwitching] Content verification timed out for first tab\")\n except Exception:\n pass\n\n await switch_tabs()\n\n # Log summary of errored tabs\n if errored_tabs:\n logger.info(f\"[TabSwitching] {len(errored_tabs)} tab(s) with errors detected. \"\n f\"Errored tabs: {[t['tab_text'] for t in errored_tabs]}\")\n else:\n logger.info(\"[TabSwitching] No tab errors detected during preload\")\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 = await self._save_debug_screenshot(page, debug_dir, \"preresize.png\")\n if pre_resize_path:\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 else:\n logger.warning(\"[DIAGNOSTIC] Failed to capture pre-resize screenshot\")\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 # Wait for charts to re-render after viewport resize\n logger.info(\"[DIAGNOSTIC] Waiting for re-render after viewport resize...\")\n await self._wait_for_resize_rendered(page, chart_count_before)\n logger.info(\"[DIAGNOSTIC] Re-render 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_success = False\n try:\n cdp = await page.context.new_cdp_session(page)\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n image_data = base64.b64decode(screenshot_data[\"data\"])\n with open(output_path, 'wb') as f:\n f.write(image_data)\n logger.info(\"Full-page screenshot saved via CDP\")\n cdp_success = True\n except Exception as cdp_err:\n logger.warning(f\"CDP screenshot failed: {cdp_err}. Falling back to Playwright full_page.\")\n await page.screenshot(path=output_path, full_page=True, timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n logger.info(\"Full-page screenshot saved via Playwright fallback\")\n cdp_success = True\n\n if cdp_success:\n # DIAGNOSTIC: Verify screenshot file\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 # 3. Screenshot errored tabs (diagnostic viewport shots)\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # Only screenshots tabs that had errors during preload, plus always keeps the main tab.\n # Errored tab screenshots are viewport-only (no full-page resize) to minimize overhead.\n if errored_tabs:\n logger.info(f\"[TabSwitching] Capturing {len(errored_tabs)} errored tab screenshot(s)...\")\n screenshotted_tabs = set()\n for tab_info in errored_tabs:\n try:\n tab_text = tab_info[\"tab_text\"]\n\n # Dedup: skip if already screenshotted\n if tab_text in screenshotted_tabs:\n continue\n screenshotted_tabs.add(tab_text)\n\n # Sanitize tab text for filename\n safe_tab = re.sub(r'[^\\w\\-_ ]', '', tab_text).strip().replace(' ', '_')[:40]\n if not safe_tab:\n safe_tab = f\"tab_{tab_info.get('depth', 0)}_{tab_info.get('tab_index', 0)}\"\n\n # Build tab path robustly (handle any extension or no extension)\n base = output_path[:-4] if output_path.lower().endswith('.png') else output_path\n tab_path = f\"{base}__tab_{safe_tab}.png\"\n\n # Switch to the errored tab — try exact text match first\n errored_tab_el = page.get_by_role(\"tab\", name=tab_text, exact=True)\n if not await errored_tab_el.is_visible():\n # Fallback: try nth-child by index\n tab_index = tab_info.get(\"tab_index\")\n if tab_index is not None:\n errored_tab_el = page.locator('.ant-tabs-tab').nth(tab_index)\n if await errored_tab_el.is_visible():\n await errored_tab_el.click()\n await asyncio.sleep(0.5)\n await self._wait_for_charts_stabilized(page, timeout_ms=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n await page.screenshot(path=tab_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n tab_size = os.path.getsize(tab_path) if os.path.exists(tab_path) else 0\n logger.info(f\"[TabSwitching] Errored tab screenshot saved: {tab_path} ({tab_size} bytes)\")\n else:\n logger.warning(f\"[TabSwitching] Errored tab '{tab_text}' not visible, skipping screenshot\")\n except Exception as tab_shot_err:\n logger.warning(f\"[TabSwitching] Failed to screenshot errored tab '{tab_info.get('tab_text')}': {tab_shot_err}\")\n\n # Return to first tab after errored tab screenshots\n try:\n first_tab_el = page.locator('.ant-tabs-nav-list .ant-tabs-tab').first\n if await first_tab_el.is_visible():\n await first_tab_el.click()\n await asyncio.sleep(0.5)\n except Exception:\n pass\n except asyncio.TimeoutError:\n logger.error(f\"Screenshot capture timed out after {SCREENSHOT_SERVICE_TIMEOUT_MS}ms\")\n try:\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n logger.info(\"Emergency timeout screenshot saved\")\n except Exception:\n pass\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=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n\n await browser.close()\n success = True\n return True\n finally:\n if success:\n shutil.rmtree(debug_dir, ignore_errors=True)\n else:\n logger.info(f\"Debug screenshots preserved in {debug_dir}\")\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n\n# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE 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 # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=LLM_HTTP_TIMEOUT_S)\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 # endregion LLMClient.__init__\n\n # region LLMClient._supports_json_response_format [TYPE 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 # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE 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: {e!s}. 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: {e!s}\")\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: {e!s}\")\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: {e!s}\")\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 # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE 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 # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE Fetch available models from the provider's API.\n # @PRE Client is initialized with provider credentials.\n # @POST Returns a list of model ID strings.\n # @SIDE_EFFECT Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models: {e}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE 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: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n\n# #endregion LLMClient\n\n# #endregion LLMAnalysisService\n" + "body": "# #region LLMAnalysisService [C:5] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]\n# @BRIEF Services for LLM interaction and dashboard screenshots.\n# @LAYER Plugin\n# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]\n# @INVARIANT Screenshots must be 1920px width and capture full page height.\n# @DATA_CONTRACT DashboardSpec -> Screenshot + Analysis\n# @RATIONALE Extracted all hardcoded timeouts into named module-level constants (PLAYWRIGHT_NAVIGATION_TIMEOUT_MS, PLAYWRIGHT_WAIT_TIMEOUT_MS, PLAYWRIGHT_SHORT_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SCREENSHOT_SERVICE_TIMEOUT_MS, LLM_HTTP_TIMEOUT_S) and DEFAULT_USER_AGENT. Zero remaining numeric timeout literals.\n\nimport asyncio\nimport base64\nimport io\nimport json\nimport os\nimport re\nimport shutil\nimport ssl\nimport tempfile\nfrom typing import Any\nfrom urllib.parse import urlsplit\n\nimport httpx\nfrom openai import AsyncOpenAI, AuthenticationError as OpenAIAuthenticationError, RateLimitError\nfrom PIL import Image\nfrom playwright.async_api import async_playwright\nfrom tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential\n\nfrom ...core.config_models import Environment\nfrom ...core.logger import belief_scope, logger\nfrom ...services.llm_prompt_templates import DEFAULT_LLM_PROMPTS, render_prompt\nfrom .models import LLMProviderType\n\n# Timeout constants (milliseconds unless noted)\nPLAYWRIGHT_NAVIGATION_TIMEOUT_MS = 30000\nPLAYWRIGHT_WAIT_TIMEOUT_MS = 10000\nPLAYWRIGHT_SHORT_TIMEOUT_MS = 5000\nHTTP_REQUEST_TIMEOUT_MS = 60000\nSCREENSHOT_SERVICE_TIMEOUT_MS = 120000\nLLM_HTTP_TIMEOUT_S = 120 # seconds (httpx client timeout)\nDEFAULT_USER_AGENT = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36\"\n\n# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE 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 # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE 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 # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE 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 # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE 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 # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE 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 # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE 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 # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE 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 # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE 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=PLAYWRIGHT_WAIT_TIMEOUT_MS,\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 # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE 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 # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE 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 # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE 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 = HTTP_REQUEST_TIMEOUT_MS,\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 # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService._wait_for_charts_stabilized [TYPE Function] [C:2]\n # @BRIEF Wait until chart elements have non-zero dimensions, with polling.\n # @PRE page is a valid Playwright page.\n # @POST Waits for chart stabilization or raises on timeout (handled internally).\n # @RATIONALE Polls for actual chart rendering dimensions rather than using a fixed delay — charts may load at different speeds depending on dashboard complexity and network conditions.\n # @REJECTED Fixed sleep-based wait rejected — would either waste time (too long) or produce blank screenshots (too short); polling for actual canvas/svg dimensions is more reliable.\n async def _wait_for_charts_stabilized(self, page, timeout_ms: int = 15000):\n \"\"\"Wait until chart elements have non-zero dimensions, with polling.\"\"\"\n # Short initial delay for rendering pipeline to start\n await asyncio.sleep(0.5)\n try:\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container canvas, .slice_container svg, .grid-content canvas');\n if (charts.length === 0) return true;\n return Array.from(charts).some(c => {\n if (c.tagName === 'CANVAS') return c.width > 10 && c.height > 10;\n if (c.tagName === 'svg') {\n const bbox = c.getBoundingClientRect();\n return bbox.width > 10 && bbox.height > 10;\n }\n return false;\n });\n }\"\"\", timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Chart stabilization wait timed out, proceeding anyway\")\n # endregion ScreenshotService._wait_for_charts_stabilized\n\n # region ScreenshotService._wait_for_resize_rendered [TYPE Function] [C:2]\n # @BRIEF Wait for charts to re-render after viewport resize.\n # @PRE page is a valid Playwright page; chart_count_before contains pre-resize element counts.\n # @POST Waits for chart content to return or timeout.\n # @RATIONALE After viewport resize, Superset triggers lazy chart re-rendering — this function polls for chart elements to reappear before taking the screenshot.\n # @REJECTED Single fixed wait after resize rejected — some dashboards re-render instantly while others take seconds; fixed wait is brittle across dashboard types.\n async def _wait_for_resize_rendered(self, page, chart_count_before: dict, timeout_ms: int = 10000):\n \"\"\"Wait for charts to re-render after viewport resize, with polling.\"\"\"\n try:\n await page.wait_for_function(\"\"\"(preCounts) => {\n const currentCharts = document.querySelectorAll('.chart-container, .slice_container').length;\n const currentCanvases = document.querySelectorAll('canvas').length;\n const currentSvgs = document.querySelectorAll('.chart-container svg, .slice_container svg').length;\n // At least one chart element must be present\n return currentCharts > 0 && (currentCanvases > 0 || currentSvgs > 0);\n }\"\"\", arg=chart_count_before, timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Re-render wait timed out after viewport resize, proceeding anyway\")\n # endregion ScreenshotService._wait_for_resize_rendered\n\n # region ScreenshotService._save_debug_screenshot [TYPE Function] [C:1]\n # @BRIEF Save a debug screenshot to a temp directory for diagnostic purposes.\n # @PRE debug_dir exists and is writable.\n # @POST Returns the debug path or None on failure.\n # @RATIONALE Uses tempfile.mkdtemp() to avoid accumulating .png files in production storage. Temp dir is cleaned up on success (rmtree) or preserved on failure for debugging.\n # @REJECTED Old approach saved debug .png next to output path — accumulated _debug_failed_login.png, _preresize.png permanently in screenshots dir (F1). In-memory-only debug logging rejected — screenshot state is visual and cannot be captured in logs.\n async def _save_debug_screenshot(self, page, debug_dir: str, suffix: str) -> str | None:\n debug_path = os.path.join(debug_dir, suffix)\n try:\n await page.screenshot(path=debug_path)\n return debug_path\n except Exception:\n return None\n # endregion ScreenshotService._save_debug_screenshot\n\n # region ScreenshotService.capture_dashboard [TYPE Function] [C:4]\n # @PURPOSE Captures a full-page screenshot of a dashboard using Playwright and CDP, plus diagnostic viewport screenshots of tabs that had chart load errors.\n # @PRE dashboard_id is a valid string, output_path is a writable path.\n # @POST Returns True if screenshot is saved successfully. Debug temp dir cleaned up on success, preserved on failure.\n # Extra diagnostic screenshots saved as output_path__tab_{tab_text}.png for each tab with chart errors.\n # @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, writes PNG files; creates and cleans up temp debug directory.\n # @UX_STATE [Navigating] -> Loading dashboard UI\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # @UX_STATE [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE [Capturing] -> Executing CDP screenshot\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # @RATIONALE Errored tab screenshots give LLM diagnostic context — an error text description (\"charts.length = 0\") is not enough to diagnose a chart load failure; a visual screenshot shows whether the tab is empty, has a loading spinner, or renders a Superset error card. Uses tempfile.mkdtemp() for all debug screenshots; cleans up on success (shutil.rmtree), preserves on failure for diagnostic analysis.\n # @REJECTED Always-screenshot-all-tabs rejected — ~20 tabs x 5s per screenshot = 100s overhead, most tabs healthy. Only errored + main gives diagnostic value at near-zero cost. Old approach saved debug .png files alongside output — accumulated permanently (F1). In-memory-only logging rejected — visual state cannot be captured in text.\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n debug_dir = tempfile.mkdtemp(prefix=\"ss_screenshot_debug_\")\n success = False\n try:\n # -- begin original body --\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=HTTP_REQUEST_TIMEOUT_MS,\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\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: {e!s}\")\n debug_path = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\n raise RuntimeError(f\"Login failed: {e!s}. 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=HTTP_REQUEST_TIMEOUT_MS,\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_dashboard_auth.png\")\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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 - wait for charts to stabilize\n logger.info(\"[DEBUG] Waiting for charts to stabilize...\")\n await self._wait_for_charts_stabilized(page)\n\n # Logic to handle tabs and full-page capture\n try:\n async with asyncio.timeout(SCREENSHOT_SERVICE_TIMEOUT_MS / 1000):\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # These are later screenshotted individually for LLM diagnostic value.\n processed_tabs = set()\n errored_tabs = [] # Collects {tab_text, depth, error} for tabs with chart load failures\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 # Wait for chart content to render in the newly active tab\n try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception:\n logger.warning(f\"[TabSwitching] Content verification timed out for tab: {tab_text}\")\n errored_tabs.append({\n \"tab_text\": tab_text,\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": \"content_verification_timeout\"\n })\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 errored_tabs.append({\n \"tab_text\": f\"tab_{depth}_{i}\",\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": f\"processing_exception: {tab_e!s}\"\n })\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 try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n except Exception:\n logger.warning(\"[TabSwitching] Content verification timed out for first tab\")\n except Exception:\n pass\n\n await switch_tabs()\n\n # Log summary of errored tabs\n if errored_tabs:\n logger.info(f\"[TabSwitching] {len(errored_tabs)} tab(s) with errors detected. \"\n f\"Errored tabs: {[t['tab_text'] for t in errored_tabs]}\")\n else:\n logger.info(\"[TabSwitching] No tab errors detected during preload\")\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 = await self._save_debug_screenshot(page, debug_dir, \"preresize.png\")\n if pre_resize_path:\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 else:\n logger.warning(\"[DIAGNOSTIC] Failed to capture pre-resize screenshot\")\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 # Wait for charts to re-render after viewport resize\n logger.info(\"[DIAGNOSTIC] Waiting for re-render after viewport resize...\")\n await self._wait_for_resize_rendered(page, chart_count_before)\n logger.info(\"[DIAGNOSTIC] Re-render 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_success = False\n try:\n cdp = await page.context.new_cdp_session(page)\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n image_data = base64.b64decode(screenshot_data[\"data\"])\n with open(output_path, 'wb') as f:\n f.write(image_data)\n logger.info(\"Full-page screenshot saved via CDP\")\n cdp_success = True\n except Exception as cdp_err:\n logger.warning(f\"CDP screenshot failed: {cdp_err}. Falling back to Playwright full_page.\")\n await page.screenshot(path=output_path, full_page=True, timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n logger.info(\"Full-page screenshot saved via Playwright fallback\")\n cdp_success = True\n\n if cdp_success:\n # DIAGNOSTIC: Verify screenshot file\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 # 3. Screenshot errored tabs (diagnostic viewport shots)\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # Only screenshots tabs that had errors during preload, plus always keeps the main tab.\n # Errored tab screenshots are viewport-only (no full-page resize) to minimize overhead.\n if errored_tabs:\n logger.info(f\"[TabSwitching] Capturing {len(errored_tabs)} errored tab screenshot(s)...\")\n screenshotted_tabs = set()\n for tab_info in errored_tabs:\n try:\n tab_text = tab_info[\"tab_text\"]\n\n # Dedup: skip if already screenshotted\n if tab_text in screenshotted_tabs:\n continue\n screenshotted_tabs.add(tab_text)\n\n # Sanitize tab text for filename\n safe_tab = re.sub(r'[^\\w\\-_ ]', '', tab_text).strip().replace(' ', '_')[:40]\n if not safe_tab:\n safe_tab = f\"tab_{tab_info.get('depth', 0)}_{tab_info.get('tab_index', 0)}\"\n\n # Build tab path robustly (handle any extension or no extension)\n base = output_path[:-4] if output_path.lower().endswith('.png') else output_path\n tab_path = f\"{base}__tab_{safe_tab}.png\"\n\n # Switch to the errored tab — try exact text match first\n errored_tab_el = page.get_by_role(\"tab\", name=tab_text, exact=True)\n if not await errored_tab_el.is_visible():\n # Fallback: try nth-child by index\n tab_index = tab_info.get(\"tab_index\")\n if tab_index is not None:\n errored_tab_el = page.locator('.ant-tabs-tab').nth(tab_index)\n if await errored_tab_el.is_visible():\n await errored_tab_el.click()\n await asyncio.sleep(0.5)\n await self._wait_for_charts_stabilized(page, timeout_ms=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n await page.screenshot(path=tab_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n tab_size = os.path.getsize(tab_path) if os.path.exists(tab_path) else 0\n logger.info(f\"[TabSwitching] Errored tab screenshot saved: {tab_path} ({tab_size} bytes)\")\n else:\n logger.warning(f\"[TabSwitching] Errored tab '{tab_text}' not visible, skipping screenshot\")\n except Exception as tab_shot_err:\n logger.warning(f\"[TabSwitching] Failed to screenshot errored tab '{tab_info.get('tab_text')}': {tab_shot_err}\")\n\n # Return to first tab after errored tab screenshots\n try:\n first_tab_el = page.locator('.ant-tabs-nav-list .ant-tabs-tab').first\n if await first_tab_el.is_visible():\n await first_tab_el.click()\n await asyncio.sleep(0.5)\n except Exception:\n pass\n except TimeoutError:\n logger.error(f\"Screenshot capture timed out after {SCREENSHOT_SERVICE_TIMEOUT_MS}ms\")\n try:\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n logger.info(\"Emergency timeout screenshot saved\")\n except Exception:\n pass\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=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n\n await browser.close()\n success = True\n return True\n finally:\n if success:\n shutil.rmtree(debug_dir, ignore_errors=True)\n else:\n logger.info(f\"Debug screenshots preserved in {debug_dir}\")\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n\n# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE 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 # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n ssl_verify = self._get_ssl_verify()\n logger.info(f\"[LLMClient.__init__] SSL Verify: {ssl_verify} (set LLM_SSL_VERIFY=false to disable)\")\n\n http_client = httpx.AsyncClient(\n headers=default_headers,\n timeout=LLM_HTTP_TIMEOUT_S,\n verify=ssl_verify,\n )\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 # endregion LLMClient.__init__\n\n # region LLMClient._get_ssl_verify [TYPE Function]\n # @PURPOSE Resolve SSL verification flag from environment.\n # @POST Returns SSLContext with system CA dir when enabled,\n # False when LLM_SSL_VERIFY env var is \"false\"/\"0\"/\"no\"/\"off\".\n # @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что\n # OpenSSL 3.x не использует intermediate CA сертификаты из cafile для\n # построения цепочки (verify code 20). capath с хеш-симлинками работает\n # корректно (verify code 0). Оба пути — cafile и capath — указывают на\n # один и тот же набор сертификатов, но capath правильно обрабатывает\n # цепочку Root → Policy → Issuing.\n # @REJECTED verify= отвергнут — httpx 0.28.x депрекейтит строковый\n # путь в verify=, требует SSLContext.\n # @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA\n # из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.\n @staticmethod\n def _get_ssl_verify() -> ssl.SSLContext | bool:\n raw = os.getenv(\"LLM_SSL_VERIFY\", \"true\").strip().lower()\n if raw in (\"false\", \"0\", \"no\", \"off\"):\n return False\n ca_dir = \"/etc/ssl/certs\"\n if os.path.isdir(ca_dir):\n return ssl.create_default_context(capath=ca_dir)\n # fallback: если директории нет (редко), используем дефолтный\n return ssl.create_default_context()\n # endregion LLMClient._get_ssl_verify\n\n # region LLMClient._format_connection_error [TYPE Function]\n # @PURPOSE Format exception chain for diagnostics, extracting httpx cause details.\n # @POST Returns a human-readable string with the full error chain.\n @staticmethod\n def _format_connection_error(exc: Exception) -> str:\n parts = [f\"{type(exc).__name__}: {exc!s}\"]\n cause = exc.__cause__ or exc.__context__\n while cause:\n parts.append(f\" └─ {type(cause).__name__}: {cause!s}\")\n cause = cause.__cause__ or cause.__context__\n return \"\\n\".join(parts)\n # endregion LLMClient._format_connection_error\n\n # region LLMClient._supports_json_response_format [TYPE 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 # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE 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: {e!s}. 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: {e!s}\")\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: {e!s}\")\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(\n f\"[get_json_completion] LLM call failed.\\n\"\n f\"{self._format_connection_error(e)}\"\n )\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 # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE 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 # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE Fetch available models from the provider's API.\n # @PRE Client is initialized with provider credentials.\n # @POST Returns a list of model ID strings.\n # @SIDE_EFFECT Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models.\\n\"\n f\"{self._format_connection_error(e)}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE 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: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n\n# #endregion LLMClient\n\n# #endregion LLMAnalysisService\n" }, { "contract_id": "ScreenshotService", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 42, - "end_line": 869, + "start_line": 41, + "end_line": 868, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35888,14 +36749,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE 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 # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE 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 # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE 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 # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE 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 # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE 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 # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE 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 # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE 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 # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE 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=PLAYWRIGHT_WAIT_TIMEOUT_MS,\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 # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE 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 # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE 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 # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE 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 = HTTP_REQUEST_TIMEOUT_MS,\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 # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService._wait_for_charts_stabilized [TYPE Function] [C:2]\n # @BRIEF Wait until chart elements have non-zero dimensions, with polling.\n # @PRE page is a valid Playwright page.\n # @POST Waits for chart stabilization or raises on timeout (handled internally).\n # @RATIONALE Polls for actual chart rendering dimensions rather than using a fixed delay — charts may load at different speeds depending on dashboard complexity and network conditions.\n # @REJECTED Fixed sleep-based wait rejected — would either waste time (too long) or produce blank screenshots (too short); polling for actual canvas/svg dimensions is more reliable.\n async def _wait_for_charts_stabilized(self, page, timeout_ms: int = 15000):\n \"\"\"Wait until chart elements have non-zero dimensions, with polling.\"\"\"\n # Short initial delay for rendering pipeline to start\n await asyncio.sleep(0.5)\n try:\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container canvas, .slice_container svg, .grid-content canvas');\n if (charts.length === 0) return true;\n return Array.from(charts).some(c => {\n if (c.tagName === 'CANVAS') return c.width > 10 && c.height > 10;\n if (c.tagName === 'svg') {\n const bbox = c.getBoundingClientRect();\n return bbox.width > 10 && bbox.height > 10;\n }\n return false;\n });\n }\"\"\", timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Chart stabilization wait timed out, proceeding anyway\")\n # endregion ScreenshotService._wait_for_charts_stabilized\n\n # region ScreenshotService._wait_for_resize_rendered [TYPE Function] [C:2]\n # @BRIEF Wait for charts to re-render after viewport resize.\n # @PRE page is a valid Playwright page; chart_count_before contains pre-resize element counts.\n # @POST Waits for chart content to return or timeout.\n # @RATIONALE After viewport resize, Superset triggers lazy chart re-rendering — this function polls for chart elements to reappear before taking the screenshot.\n # @REJECTED Single fixed wait after resize rejected — some dashboards re-render instantly while others take seconds; fixed wait is brittle across dashboard types.\n async def _wait_for_resize_rendered(self, page, chart_count_before: dict, timeout_ms: int = 10000):\n \"\"\"Wait for charts to re-render after viewport resize, with polling.\"\"\"\n try:\n await page.wait_for_function(\"\"\"(preCounts) => {\n const currentCharts = document.querySelectorAll('.chart-container, .slice_container').length;\n const currentCanvases = document.querySelectorAll('canvas').length;\n const currentSvgs = document.querySelectorAll('.chart-container svg, .slice_container svg').length;\n // At least one chart element must be present\n return currentCharts > 0 && (currentCanvases > 0 || currentSvgs > 0);\n }\"\"\", arg=chart_count_before, timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Re-render wait timed out after viewport resize, proceeding anyway\")\n # endregion ScreenshotService._wait_for_resize_rendered\n\n # region ScreenshotService._save_debug_screenshot [TYPE Function] [C:1]\n # @BRIEF Save a debug screenshot to a temp directory for diagnostic purposes.\n # @PRE debug_dir exists and is writable.\n # @POST Returns the debug path or None on failure.\n # @RATIONALE Uses tempfile.mkdtemp() to avoid accumulating .png files in production storage. Temp dir is cleaned up on success (rmtree) or preserved on failure for debugging.\n # @REJECTED Old approach saved debug .png next to output path — accumulated _debug_failed_login.png, _preresize.png permanently in screenshots dir (F1). In-memory-only debug logging rejected — screenshot state is visual and cannot be captured in logs.\n async def _save_debug_screenshot(self, page, debug_dir: str, suffix: str) -> str | None:\n debug_path = os.path.join(debug_dir, suffix)\n try:\n await page.screenshot(path=debug_path)\n return debug_path\n except Exception:\n return None\n # endregion ScreenshotService._save_debug_screenshot\n\n # region ScreenshotService.capture_dashboard [TYPE Function] [C:4]\n # @PURPOSE Captures a full-page screenshot of a dashboard using Playwright and CDP, plus diagnostic viewport screenshots of tabs that had chart load errors.\n # @PRE dashboard_id is a valid string, output_path is a writable path.\n # @POST Returns True if screenshot is saved successfully. Debug temp dir cleaned up on success, preserved on failure.\n # Extra diagnostic screenshots saved as output_path__tab_{tab_text}.png for each tab with chart errors.\n # @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, writes PNG files; creates and cleans up temp debug directory.\n # @UX_STATE [Navigating] -> Loading dashboard UI\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # @UX_STATE [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE [Capturing] -> Executing CDP screenshot\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # @RATIONALE Errored tab screenshots give LLM diagnostic context — an error text description (\"charts.length = 0\") is not enough to diagnose a chart load failure; a visual screenshot shows whether the tab is empty, has a loading spinner, or renders a Superset error card. Uses tempfile.mkdtemp() for all debug screenshots; cleans up on success (shutil.rmtree), preserves on failure for diagnostic analysis.\n # @REJECTED Always-screenshot-all-tabs rejected — ~20 tabs x 5s per screenshot = 100s overhead, most tabs healthy. Only errored + main gives diagnostic value at near-zero cost. Old approach saved debug .png files alongside output — accumulated permanently (F1). In-memory-only logging rejected — visual state cannot be captured in text.\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n debug_dir = tempfile.mkdtemp(prefix=\"ss_screenshot_debug_\")\n success = False\n try:\n # -- begin original body --\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=HTTP_REQUEST_TIMEOUT_MS,\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\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: {e!s}\")\n debug_path = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\n raise RuntimeError(f\"Login failed: {e!s}. 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=HTTP_REQUEST_TIMEOUT_MS,\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_dashboard_auth.png\")\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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 - wait for charts to stabilize\n logger.info(\"[DEBUG] Waiting for charts to stabilize...\")\n await self._wait_for_charts_stabilized(page)\n\n # Logic to handle tabs and full-page capture\n try:\n async with asyncio.timeout(SCREENSHOT_SERVICE_TIMEOUT_MS / 1000):\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # These are later screenshotted individually for LLM diagnostic value.\n processed_tabs = set()\n errored_tabs = [] # Collects {tab_text, depth, error} for tabs with chart load failures\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 # Wait for chart content to render in the newly active tab\n try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception:\n logger.warning(f\"[TabSwitching] Content verification timed out for tab: {tab_text}\")\n errored_tabs.append({\n \"tab_text\": tab_text,\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": \"content_verification_timeout\"\n })\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 errored_tabs.append({\n \"tab_text\": f\"tab_{depth}_{i}\",\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": f\"processing_exception: {tab_e!s}\"\n })\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 try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n except Exception:\n logger.warning(\"[TabSwitching] Content verification timed out for first tab\")\n except Exception:\n pass\n\n await switch_tabs()\n\n # Log summary of errored tabs\n if errored_tabs:\n logger.info(f\"[TabSwitching] {len(errored_tabs)} tab(s) with errors detected. \"\n f\"Errored tabs: {[t['tab_text'] for t in errored_tabs]}\")\n else:\n logger.info(\"[TabSwitching] No tab errors detected during preload\")\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 = await self._save_debug_screenshot(page, debug_dir, \"preresize.png\")\n if pre_resize_path:\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 else:\n logger.warning(\"[DIAGNOSTIC] Failed to capture pre-resize screenshot\")\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 # Wait for charts to re-render after viewport resize\n logger.info(\"[DIAGNOSTIC] Waiting for re-render after viewport resize...\")\n await self._wait_for_resize_rendered(page, chart_count_before)\n logger.info(\"[DIAGNOSTIC] Re-render 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_success = False\n try:\n cdp = await page.context.new_cdp_session(page)\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n image_data = base64.b64decode(screenshot_data[\"data\"])\n with open(output_path, 'wb') as f:\n f.write(image_data)\n logger.info(\"Full-page screenshot saved via CDP\")\n cdp_success = True\n except Exception as cdp_err:\n logger.warning(f\"CDP screenshot failed: {cdp_err}. Falling back to Playwright full_page.\")\n await page.screenshot(path=output_path, full_page=True, timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n logger.info(\"Full-page screenshot saved via Playwright fallback\")\n cdp_success = True\n\n if cdp_success:\n # DIAGNOSTIC: Verify screenshot file\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 # 3. Screenshot errored tabs (diagnostic viewport shots)\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # Only screenshots tabs that had errors during preload, plus always keeps the main tab.\n # Errored tab screenshots are viewport-only (no full-page resize) to minimize overhead.\n if errored_tabs:\n logger.info(f\"[TabSwitching] Capturing {len(errored_tabs)} errored tab screenshot(s)...\")\n screenshotted_tabs = set()\n for tab_info in errored_tabs:\n try:\n tab_text = tab_info[\"tab_text\"]\n\n # Dedup: skip if already screenshotted\n if tab_text in screenshotted_tabs:\n continue\n screenshotted_tabs.add(tab_text)\n\n # Sanitize tab text for filename\n safe_tab = re.sub(r'[^\\w\\-_ ]', '', tab_text).strip().replace(' ', '_')[:40]\n if not safe_tab:\n safe_tab = f\"tab_{tab_info.get('depth', 0)}_{tab_info.get('tab_index', 0)}\"\n\n # Build tab path robustly (handle any extension or no extension)\n base = output_path[:-4] if output_path.lower().endswith('.png') else output_path\n tab_path = f\"{base}__tab_{safe_tab}.png\"\n\n # Switch to the errored tab — try exact text match first\n errored_tab_el = page.get_by_role(\"tab\", name=tab_text, exact=True)\n if not await errored_tab_el.is_visible():\n # Fallback: try nth-child by index\n tab_index = tab_info.get(\"tab_index\")\n if tab_index is not None:\n errored_tab_el = page.locator('.ant-tabs-tab').nth(tab_index)\n if await errored_tab_el.is_visible():\n await errored_tab_el.click()\n await asyncio.sleep(0.5)\n await self._wait_for_charts_stabilized(page, timeout_ms=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n await page.screenshot(path=tab_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n tab_size = os.path.getsize(tab_path) if os.path.exists(tab_path) else 0\n logger.info(f\"[TabSwitching] Errored tab screenshot saved: {tab_path} ({tab_size} bytes)\")\n else:\n logger.warning(f\"[TabSwitching] Errored tab '{tab_text}' not visible, skipping screenshot\")\n except Exception as tab_shot_err:\n logger.warning(f\"[TabSwitching] Failed to screenshot errored tab '{tab_info.get('tab_text')}': {tab_shot_err}\")\n\n # Return to first tab after errored tab screenshots\n try:\n first_tab_el = page.locator('.ant-tabs-nav-list .ant-tabs-tab').first\n if await first_tab_el.is_visible():\n await first_tab_el.click()\n await asyncio.sleep(0.5)\n except Exception:\n pass\n except asyncio.TimeoutError:\n logger.error(f\"Screenshot capture timed out after {SCREENSHOT_SERVICE_TIMEOUT_MS}ms\")\n try:\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n logger.info(\"Emergency timeout screenshot saved\")\n except Exception:\n pass\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=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n\n await browser.close()\n success = True\n return True\n finally:\n if success:\n shutil.rmtree(debug_dir, ignore_errors=True)\n else:\n logger.info(f\"Debug screenshots preserved in {debug_dir}\")\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n" + "body": "# #region ScreenshotService [TYPE Class]\n# @BRIEF Handles capturing screenshots of Superset dashboards.\nclass ScreenshotService:\n # region ScreenshotService.__init__ [TYPE 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 # endregion ScreenshotService.__init__\n\n # region ScreenshotService._find_first_visible_locator [TYPE 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 # endregion ScreenshotService._find_first_visible_locator\n\n # region ScreenshotService._iter_login_roots [TYPE 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 # endregion ScreenshotService._iter_login_roots\n\n # region ScreenshotService._extract_hidden_login_fields [TYPE 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 # endregion ScreenshotService._extract_hidden_login_fields\n\n # region ScreenshotService._extract_csrf_token [TYPE 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 # endregion ScreenshotService._extract_csrf_token\n\n # region ScreenshotService._response_looks_like_login_page [TYPE 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 # endregion ScreenshotService._response_looks_like_login_page\n\n # region ScreenshotService._redirect_looks_authenticated [TYPE 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 # endregion ScreenshotService._redirect_looks_authenticated\n\n # region ScreenshotService._submit_login_via_form_post [TYPE 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=PLAYWRIGHT_WAIT_TIMEOUT_MS,\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 # endregion ScreenshotService._submit_login_via_form_post\n\n # region ScreenshotService._find_login_field_locator [TYPE 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 # endregion ScreenshotService._find_login_field_locator\n\n # region ScreenshotService._find_submit_locator [TYPE 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 # endregion ScreenshotService._find_submit_locator\n\n # region ScreenshotService._goto_resilient [TYPE 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 = HTTP_REQUEST_TIMEOUT_MS,\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 # endregion ScreenshotService._goto_resilient\n\n # region ScreenshotService._wait_for_charts_stabilized [TYPE Function] [C:2]\n # @BRIEF Wait until chart elements have non-zero dimensions, with polling.\n # @PRE page is a valid Playwright page.\n # @POST Waits for chart stabilization or raises on timeout (handled internally).\n # @RATIONALE Polls for actual chart rendering dimensions rather than using a fixed delay — charts may load at different speeds depending on dashboard complexity and network conditions.\n # @REJECTED Fixed sleep-based wait rejected — would either waste time (too long) or produce blank screenshots (too short); polling for actual canvas/svg dimensions is more reliable.\n async def _wait_for_charts_stabilized(self, page, timeout_ms: int = 15000):\n \"\"\"Wait until chart elements have non-zero dimensions, with polling.\"\"\"\n # Short initial delay for rendering pipeline to start\n await asyncio.sleep(0.5)\n try:\n await page.wait_for_function(\"\"\"() => {\n const charts = document.querySelectorAll('.chart-container canvas, .slice_container svg, .grid-content canvas');\n if (charts.length === 0) return true;\n return Array.from(charts).some(c => {\n if (c.tagName === 'CANVAS') return c.width > 10 && c.height > 10;\n if (c.tagName === 'svg') {\n const bbox = c.getBoundingClientRect();\n return bbox.width > 10 && bbox.height > 10;\n }\n return false;\n });\n }\"\"\", timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Chart stabilization wait timed out, proceeding anyway\")\n # endregion ScreenshotService._wait_for_charts_stabilized\n\n # region ScreenshotService._wait_for_resize_rendered [TYPE Function] [C:2]\n # @BRIEF Wait for charts to re-render after viewport resize.\n # @PRE page is a valid Playwright page; chart_count_before contains pre-resize element counts.\n # @POST Waits for chart content to return or timeout.\n # @RATIONALE After viewport resize, Superset triggers lazy chart re-rendering — this function polls for chart elements to reappear before taking the screenshot.\n # @REJECTED Single fixed wait after resize rejected — some dashboards re-render instantly while others take seconds; fixed wait is brittle across dashboard types.\n async def _wait_for_resize_rendered(self, page, chart_count_before: dict, timeout_ms: int = 10000):\n \"\"\"Wait for charts to re-render after viewport resize, with polling.\"\"\"\n try:\n await page.wait_for_function(\"\"\"(preCounts) => {\n const currentCharts = document.querySelectorAll('.chart-container, .slice_container').length;\n const currentCanvases = document.querySelectorAll('canvas').length;\n const currentSvgs = document.querySelectorAll('.chart-container svg, .slice_container svg').length;\n // At least one chart element must be present\n return currentCharts > 0 && (currentCanvases > 0 || currentSvgs > 0);\n }\"\"\", arg=chart_count_before, timeout=timeout_ms)\n except Exception:\n logger.warning(\"[ScreenshotService] Re-render wait timed out after viewport resize, proceeding anyway\")\n # endregion ScreenshotService._wait_for_resize_rendered\n\n # region ScreenshotService._save_debug_screenshot [TYPE Function] [C:1]\n # @BRIEF Save a debug screenshot to a temp directory for diagnostic purposes.\n # @PRE debug_dir exists and is writable.\n # @POST Returns the debug path or None on failure.\n # @RATIONALE Uses tempfile.mkdtemp() to avoid accumulating .png files in production storage. Temp dir is cleaned up on success (rmtree) or preserved on failure for debugging.\n # @REJECTED Old approach saved debug .png next to output path — accumulated _debug_failed_login.png, _preresize.png permanently in screenshots dir (F1). In-memory-only debug logging rejected — screenshot state is visual and cannot be captured in logs.\n async def _save_debug_screenshot(self, page, debug_dir: str, suffix: str) -> str | None:\n debug_path = os.path.join(debug_dir, suffix)\n try:\n await page.screenshot(path=debug_path)\n return debug_path\n except Exception:\n return None\n # endregion ScreenshotService._save_debug_screenshot\n\n # region ScreenshotService.capture_dashboard [TYPE Function] [C:4]\n # @PURPOSE Captures a full-page screenshot of a dashboard using Playwright and CDP, plus diagnostic viewport screenshots of tabs that had chart load errors.\n # @PRE dashboard_id is a valid string, output_path is a writable path.\n # @POST Returns True if screenshot is saved successfully. Debug temp dir cleaned up on success, preserved on failure.\n # Extra diagnostic screenshots saved as output_path__tab_{tab_text}.png for each tab with chart errors.\n # @SIDE_EFFECT Launches a browser, performs UI login, switches tabs, writes PNG files; creates and cleans up temp debug directory.\n # @UX_STATE [Navigating] -> Loading dashboard UI\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # @UX_STATE [CalculatingHeight] -> Determining dashboard dimensions\n # @UX_STATE [Capturing] -> Executing CDP screenshot\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # @RATIONALE Errored tab screenshots give LLM diagnostic context — an error text description (\"charts.length = 0\") is not enough to diagnose a chart load failure; a visual screenshot shows whether the tab is empty, has a loading spinner, or renders a Superset error card. Uses tempfile.mkdtemp() for all debug screenshots; cleans up on success (shutil.rmtree), preserves on failure for diagnostic analysis.\n # @REJECTED Always-screenshot-all-tabs rejected — ~20 tabs x 5s per screenshot = 100s overhead, most tabs healthy. Only errored + main gives diagnostic value at near-zero cost. Old approach saved debug .png files alongside output — accumulated permanently (F1). In-memory-only logging rejected — visual state cannot be captured in text.\n async def capture_dashboard(self, dashboard_id: str, output_path: str) -> bool:\n debug_dir = tempfile.mkdtemp(prefix=\"ss_screenshot_debug_\")\n success = False\n try:\n # -- begin original body --\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=HTTP_REQUEST_TIMEOUT_MS,\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\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: {e!s}\")\n debug_path = await self._save_debug_screenshot(page, debug_dir, \"failed_login.png\")\n raise RuntimeError(f\"Login failed: {e!s}. 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=HTTP_REQUEST_TIMEOUT_MS,\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 = await self._save_debug_screenshot(page, debug_dir, \"failed_dashboard_auth.png\")\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=PLAYWRIGHT_NAVIGATION_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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=HTTP_REQUEST_TIMEOUT_MS)\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 - wait for charts to stabilize\n logger.info(\"[DEBUG] Waiting for charts to stabilize...\")\n await self._wait_for_charts_stabilized(page)\n\n # Logic to handle tabs and full-page capture\n try:\n async with asyncio.timeout(SCREENSHOT_SERVICE_TIMEOUT_MS / 1000):\n # 1. Handle Tabs (Recursive switching)\n # @UX_STATE [TabSwitching] -> Iterating through dashboard tabs to trigger lazy loading.\n # Collects errored_tabs — those where chart content fails to render within timeout.\n # These are later screenshotted individually for LLM diagnostic value.\n processed_tabs = set()\n errored_tabs = [] # Collects {tab_text, depth, error} for tabs with chart load failures\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 # Wait for chart content to render in the newly active tab\n try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception:\n logger.warning(f\"[TabSwitching] Content verification timed out for tab: {tab_text}\")\n errored_tabs.append({\n \"tab_text\": tab_text,\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": \"content_verification_timeout\"\n })\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 errored_tabs.append({\n \"tab_text\": f\"tab_{depth}_{i}\",\n \"tab_index\": i,\n \"depth\": depth,\n \"error\": f\"processing_exception: {tab_e!s}\"\n })\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 try:\n await page.wait_for_function(\"\"\"() => {\n const activeTab = document.querySelector('.ant-tabs-tab-active');\n if (!activeTab) return true;\n const tabPane = activeTab.closest('.ant-tabs')?.querySelector('.ant-tabs-content-holder');\n if (!tabPane) return true;\n const charts = tabPane.querySelectorAll('canvas, svg');\n return charts.length > 0;\n }\"\"\", timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n except Exception:\n logger.warning(\"[TabSwitching] Content verification timed out for first tab\")\n except Exception:\n pass\n\n await switch_tabs()\n\n # Log summary of errored tabs\n if errored_tabs:\n logger.info(f\"[TabSwitching] {len(errored_tabs)} tab(s) with errors detected. \"\n f\"Errored tabs: {[t['tab_text'] for t in errored_tabs]}\")\n else:\n logger.info(\"[TabSwitching] No tab errors detected during preload\")\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 = await self._save_debug_screenshot(page, debug_dir, \"preresize.png\")\n if pre_resize_path:\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 else:\n logger.warning(\"[DIAGNOSTIC] Failed to capture pre-resize screenshot\")\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 # Wait for charts to re-render after viewport resize\n logger.info(\"[DIAGNOSTIC] Waiting for re-render after viewport resize...\")\n await self._wait_for_resize_rendered(page, chart_count_before)\n logger.info(\"[DIAGNOSTIC] Re-render 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_success = False\n try:\n cdp = await page.context.new_cdp_session(page)\n screenshot_data = await cdp.send(\"Page.captureScreenshot\", {\n \"format\": \"png\",\n \"fromSurface\": True,\n \"captureBeyondViewport\": True\n })\n image_data = base64.b64decode(screenshot_data[\"data\"])\n with open(output_path, 'wb') as f:\n f.write(image_data)\n logger.info(\"Full-page screenshot saved via CDP\")\n cdp_success = True\n except Exception as cdp_err:\n logger.warning(f\"CDP screenshot failed: {cdp_err}. Falling back to Playwright full_page.\")\n await page.screenshot(path=output_path, full_page=True, timeout=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n logger.info(\"Full-page screenshot saved via Playwright fallback\")\n cdp_success = True\n\n if cdp_success:\n # DIAGNOSTIC: Verify screenshot file\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 # 3. Screenshot errored tabs (diagnostic viewport shots)\n # @UX_STATE [CapturingErroredTabs] -> Taking viewport screenshots of tabs with chart errors.\n # Only screenshots tabs that had errors during preload, plus always keeps the main tab.\n # Errored tab screenshots are viewport-only (no full-page resize) to minimize overhead.\n if errored_tabs:\n logger.info(f\"[TabSwitching] Capturing {len(errored_tabs)} errored tab screenshot(s)...\")\n screenshotted_tabs = set()\n for tab_info in errored_tabs:\n try:\n tab_text = tab_info[\"tab_text\"]\n\n # Dedup: skip if already screenshotted\n if tab_text in screenshotted_tabs:\n continue\n screenshotted_tabs.add(tab_text)\n\n # Sanitize tab text for filename\n safe_tab = re.sub(r'[^\\w\\-_ ]', '', tab_text).strip().replace(' ', '_')[:40]\n if not safe_tab:\n safe_tab = f\"tab_{tab_info.get('depth', 0)}_{tab_info.get('tab_index', 0)}\"\n\n # Build tab path robustly (handle any extension or no extension)\n base = output_path[:-4] if output_path.lower().endswith('.png') else output_path\n tab_path = f\"{base}__tab_{safe_tab}.png\"\n\n # Switch to the errored tab — try exact text match first\n errored_tab_el = page.get_by_role(\"tab\", name=tab_text, exact=True)\n if not await errored_tab_el.is_visible():\n # Fallback: try nth-child by index\n tab_index = tab_info.get(\"tab_index\")\n if tab_index is not None:\n errored_tab_el = page.locator('.ant-tabs-tab').nth(tab_index)\n if await errored_tab_el.is_visible():\n await errored_tab_el.click()\n await asyncio.sleep(0.5)\n await self._wait_for_charts_stabilized(page, timeout_ms=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n await page.screenshot(path=tab_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n tab_size = os.path.getsize(tab_path) if os.path.exists(tab_path) else 0\n logger.info(f\"[TabSwitching] Errored tab screenshot saved: {tab_path} ({tab_size} bytes)\")\n else:\n logger.warning(f\"[TabSwitching] Errored tab '{tab_text}' not visible, skipping screenshot\")\n except Exception as tab_shot_err:\n logger.warning(f\"[TabSwitching] Failed to screenshot errored tab '{tab_info.get('tab_text')}': {tab_shot_err}\")\n\n # Return to first tab after errored tab screenshots\n try:\n first_tab_el = page.locator('.ant-tabs-nav-list .ant-tabs-tab').first\n if await first_tab_el.is_visible():\n await first_tab_el.click()\n await asyncio.sleep(0.5)\n except Exception:\n pass\n except TimeoutError:\n logger.error(f\"Screenshot capture timed out after {SCREENSHOT_SERVICE_TIMEOUT_MS}ms\")\n try:\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n logger.info(\"Emergency timeout screenshot saved\")\n except Exception:\n pass\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=PLAYWRIGHT_WAIT_TIMEOUT_MS)\n except Exception as e2:\n logger.error(f\"[DEBUG] Fallback screenshot also failed: {e2}\")\n await page.screenshot(path=output_path, timeout=PLAYWRIGHT_SHORT_TIMEOUT_MS)\n\n await browser.close()\n success = True\n return True\n finally:\n if success:\n shutil.rmtree(debug_dir, ignore_errors=True)\n else:\n logger.info(f\"Debug screenshots preserved in {debug_dir}\")\n # endregion ScreenshotService.capture_dashboard\n# #endregion ScreenshotService\n" }, { "contract_id": "LLMClient", "contract_type": "Class", "file_path": "backend/src/plugins/llm_analysis/service.py", - "start_line": 871, - "end_line": 1172, + "start_line": 870, + "end_line": 1221, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -35905,7 +36766,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE 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 # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n http_client = httpx.AsyncClient(headers=default_headers, timeout=LLM_HTTP_TIMEOUT_S)\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 # endregion LLMClient.__init__\n\n # region LLMClient._supports_json_response_format [TYPE 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 # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE 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: {e!s}. 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: {e!s}\")\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: {e!s}\")\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: {e!s}\")\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 # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE 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 # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE Fetch available models from the provider's API.\n # @PRE Client is initialized with provider credentials.\n # @POST Returns a list of model ID strings.\n # @SIDE_EFFECT Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models: {e}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE 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: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n\n# #endregion LLMClient\n" + "body": "# #region LLMClient [TYPE Class]\n# @BRIEF Wrapper for LLM provider APIs.\nclass LLMClient:\n # region LLMClient.__init__ [TYPE 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 # LiteLLM proxy uses standard OpenAI-compatible Bearer auth — no special headers needed.\n # It routes to upstream providers transparently, and the default Authorization header\n # is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.\n\n ssl_verify = self._get_ssl_verify()\n logger.info(f\"[LLMClient.__init__] SSL Verify: {ssl_verify} (set LLM_SSL_VERIFY=false to disable)\")\n\n http_client = httpx.AsyncClient(\n headers=default_headers,\n timeout=LLM_HTTP_TIMEOUT_S,\n verify=ssl_verify,\n )\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 # endregion LLMClient.__init__\n\n # region LLMClient._get_ssl_verify [TYPE Function]\n # @PURPOSE Resolve SSL verification flag from environment.\n # @POST Returns SSLContext with system CA dir when enabled,\n # False when LLM_SSL_VERIFY env var is \"false\"/\"0\"/\"no\"/\"off\".\n # @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что\n # OpenSSL 3.x не использует intermediate CA сертификаты из cafile для\n # построения цепочки (verify code 20). capath с хеш-симлинками работает\n # корректно (verify code 0). Оба пути — cafile и capath — указывают на\n # один и тот же набор сертификатов, но capath правильно обрабатывает\n # цепочку Root → Policy → Issuing.\n # @REJECTED verify= отвергнут — httpx 0.28.x депрекейтит строковый\n # путь в verify=, требует SSLContext.\n # @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA\n # из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.\n @staticmethod\n def _get_ssl_verify() -> ssl.SSLContext | bool:\n raw = os.getenv(\"LLM_SSL_VERIFY\", \"true\").strip().lower()\n if raw in (\"false\", \"0\", \"no\", \"off\"):\n return False\n ca_dir = \"/etc/ssl/certs\"\n if os.path.isdir(ca_dir):\n return ssl.create_default_context(capath=ca_dir)\n # fallback: если директории нет (редко), используем дефолтный\n return ssl.create_default_context()\n # endregion LLMClient._get_ssl_verify\n\n # region LLMClient._format_connection_error [TYPE Function]\n # @PURPOSE Format exception chain for diagnostics, extracting httpx cause details.\n # @POST Returns a human-readable string with the full error chain.\n @staticmethod\n def _format_connection_error(exc: Exception) -> str:\n parts = [f\"{type(exc).__name__}: {exc!s}\"]\n cause = exc.__cause__ or exc.__context__\n while cause:\n parts.append(f\" └─ {type(cause).__name__}: {cause!s}\")\n cause = cause.__cause__ or cause.__context__\n return \"\\n\".join(parts)\n # endregion LLMClient._format_connection_error\n\n # region LLMClient._supports_json_response_format [TYPE 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 # endregion LLMClient._supports_json_response_format\n\n # region LLMClient.get_json_completion [TYPE 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: {e!s}. 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: {e!s}\")\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: {e!s}\")\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(\n f\"[get_json_completion] LLM call failed.\\n\"\n f\"{self._format_connection_error(e)}\"\n )\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 # endregion LLMClient.get_json_completion\n\n # region LLMClient.test_runtime_connection [TYPE 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 # endregion LLMClient.test_runtime_connection\n\n # region LLMClient.fetch_models [TYPE Function]\n # @PURPOSE Fetch available models from the provider's API.\n # @PRE Client is initialized with provider credentials.\n # @POST Returns a list of model ID strings.\n # @SIDE_EFFECT Calls external LLM API /v1/models endpoint.\n async def fetch_models(self) -> list[str]:\n with belief_scope(\"LLMClient.fetch_models\"):\n try:\n response = await self.client.models.list()\n model_ids = [m.id for m in response.data]\n model_ids.sort()\n logger.reason(\n f\"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}\",\n extra={\"src\": \"LLMClient.fetch_models\"},\n )\n return model_ids\n except Exception as e:\n logger.warning(\n f\"[LLMClient.fetch_models] Failed to fetch models.\\n\"\n f\"{self._format_connection_error(e)}\",\n )\n raise\n # endregion LLMClient.fetch_models\n\n # region LLMClient.analyze_dashboard [TYPE 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: {e!s}\")\n return {\n \"status\": \"UNKNOWN\",\n \"summary\": f\"Failed to get response from LLM: {e!s}\",\n \"issues\": [{\"severity\": \"UNKNOWN\", \"message\": \"LLM provider returned empty or invalid response\"}]\n }\n # endregion LLMClient.analyze_dashboard\n\n# #endregion LLMClient\n" }, { "contract_id": "MaintenanceBannerPlugin", @@ -36137,8 +36998,8 @@ "contract_id": "TestClickHouseTimestampNormalization", "contract_type": "Class", "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", - "start_line": 85, - "end_line": 145, + "start_line": 84, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36154,8 +37015,8 @@ "contract_id": "TestClickHouseInsertSqlGeneration", "contract_type": "Class", "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", - "start_line": 148, - "end_line": 257, + "start_line": 147, + "end_line": 256, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36171,8 +37032,8 @@ "contract_id": "TestClickHouseJoinVerification", "contract_type": "Class", "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", - "start_line": 260, - "end_line": 328, + "start_line": 259, + "end_line": 327, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36188,8 +37049,8 @@ "contract_id": "TestOrchestratorInsertFlow", "contract_type": "Class", "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", - "start_line": 331, - "end_line": 420, + "start_line": 330, + "end_line": 419, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36205,8 +37066,8 @@ "contract_id": "TestClickHouseEndToEnd", "contract_type": "Class", "file_path": "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py", - "start_line": 423, - "end_line": 543, + "start_line": 422, + "end_line": 542, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36234,14 +37095,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDictionaryLegacyHub [C:3] [TYPE Module] [SEMANTICS test, dictionary, hub]\n# @BRIEF Legacy test module — all tests migrated to domain-specific test files:\n# test_dictionary_crud.py — DictionaryCRUD + DictionaryEntryCRUD\n# test_dictionary_import.py — Import/export operations\n# test_dictionary_filter.py — Batch filter + migration\n# test_dictionary_correction.py — Correction context capture\n# test_dictionary_prompt_builder.py — Prompt builder operations\n# test_dictionary_utils.py — Utility functions\n# @RELATION BINDS_TO -> [DictionaryManager]\n# @RATIONALE Split monolithic test module into domain-specific files per INV_7.\n# @REJECTED Keeping 1199-line test module violates module < 400 lines constraint.\n\n# All tests have been migrated to the following modules:\nfrom .test_dictionary_correction import * # noqa: F401, F403\nfrom .test_dictionary_crud import * # noqa: F401, F403\nfrom .test_dictionary_filter import * # noqa: F401, F403\nfrom .test_dictionary_import import * # noqa: F401, F403\nfrom .test_dictionary_prompt_builder import * # noqa: F401, F403\nfrom .test_dictionary_utils import * # noqa: F401, F403\n# #endregion TestDictionaryLegacyHub\n" + "body": "# #region TestDictionaryLegacyHub [C:3] [TYPE Module] [SEMANTICS test, dictionary, hub]\n# @BRIEF Legacy test module — all tests migrated to domain-specific test files:\n# test_dictionary_crud.py — DictionaryCRUD + DictionaryEntryCRUD\n# test_dictionary_import.py — Import/export operations\n# test_dictionary_filter.py — Batch filter + migration\n# test_dictionary_correction.py — Correction context capture\n# test_dictionary_prompt_builder.py — Prompt builder operations\n# test_dictionary_utils.py — Utility functions\n# @RELATION BINDS_TO -> [DictionaryManager]\n# @RATIONALE Split monolithic test module into domain-specific files per INV_7.\n# @REJECTED Keeping 1199-line test module violates module < 400 lines constraint.\n\n# All tests have been migrated to the following modules:\nfrom .test_dictionary_correction import * # noqa: F403\nfrom .test_dictionary_crud import * # noqa: F403\nfrom .test_dictionary_filter import * # noqa: F403\nfrom .test_dictionary_import import * # noqa: F403\nfrom .test_dictionary_prompt_builder import * # noqa: F403\nfrom .test_dictionary_utils import * # noqa: F403\n# #endregion TestDictionaryLegacyHub\n" }, { "contract_id": "TestDictionaryCorrection", "contract_type": "Module", "file_path": "backend/src/plugins/translate/__tests__/test_dictionary_correction.py", "start_line": 1, - "end_line": 125, + "end_line": 124, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36259,7 +37120,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDictionaryCorrection [C:3] [TYPE Module] [SEMANTICS test, dictionary, correction, context]\n# @BRIEF Validate InlineCorrectionService and dictionary correction flows.\n# @RELATION BINDS_TO -> [InlineCorrectionService]\n\nimport pytest\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import (\n Base,\n DictionaryEntry,\n TerminologyDictionary,\n TranslationBatch,\n TranslationJob,\n TranslationLanguage,\n TranslationRecord,\n TranslationRun,\n)\nfrom src.plugins.translate.dictionary import DictionaryManager\nfrom src.plugins.translate.service import InlineCorrectionService\n\n\n# region db_session [TYPE Fixture]\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestContextCapture:\n \"\"\"Verify context auto-capture when submitting corrections.\"\"\"\n\n # region test_context_capture_in_correction [C:2] [TYPE Function]\n def test_context_capture_in_correction(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Ctx Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n\n run = TranslationRun(job_id=job.id, status=\"COMPLETED\")\n db_session.add(run)\n db_session.flush()\n\n batch = TranslationBatch(run_id=run.id, batch_index=0, status=\"COMPLETED\")\n db_session.add(batch)\n db_session.flush()\n\n record = TranslationRecord(\n batch_id=batch.id, run_id=run.id, source_sql=\"hello world\",\n source_object_type=\"table_row\", source_object_name=\"Row 0\",\n source_object_id=\"0\", source_data={\"schema\": \"public\", \"table\": \"users\", \"column\": \"name\"},\n status=\"SUCCESS\",\n )\n db_session.add(record)\n db_session.flush()\n\n for lang_code in (\"ru\", \"de\", \"fr\", \"it\"):\n tl = TranslationLanguage(\n record_id=record.id, language_code=lang_code,\n source_language_detected=\"en\", translated_value=\"translated\",\n final_value=\"translated\", status=\"translated\",\n )\n db_session.add(tl)\n db_session.commit()\n\n result = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"ru\",\n dictionary_id=d.id, corrected_value=\"привет мир (исправлено)\",\n current_user=\"test_user\",\n )\n assert result[\"action\"] == \"created\"\n assert result[\"entry_id\"] is not None\n\n entry = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result[\"entry_id\"]).first()\n assert entry is not None\n assert entry.has_context is True\n assert entry.context_source == \"auto\"\n assert entry.context_data is not None\n assert \"source_data\" in entry.context_data\n assert entry.context_data[\"source_object_type\"] == \"table_row\"\n\n result2 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"de\",\n dictionary_id=d.id, corrected_value=\"another correction\",\n current_user=\"test_user\", context_data_override={\"custom_key\": \"custom_value\"},\n )\n assert result2[\"action\"] == \"created\"\n entry2 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result2[\"entry_id\"]).first()\n assert entry2.context_data == {\"custom_key\": \"custom_value\"}\n assert entry2.context_source == \"manual\"\n\n result3 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"fr\",\n dictionary_id=d.id, corrected_value=\"no context\",\n current_user=\"test_user\", keep_context=False,\n )\n assert result3[\"action\"] == \"created\"\n entry3 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result3[\"entry_id\"]).first()\n assert entry3.context_data is None\n assert entry3.has_context is False\n assert entry3.context_source == \"manual\"\n\n result4 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"it\",\n dictionary_id=d.id, corrected_value=\"with notes\",\n current_user=\"test_user\", usage_notes=\"Use this for user-facing columns only\",\n )\n assert result4[\"action\"] == \"created\"\n entry4 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result4[\"entry_id\"]).first()\n assert entry4.usage_notes == \"Use this for user-facing columns only\"\n # endregion test_context_capture_in_correction\n# #endregion TestDictionaryCorrection\n" + "body": "# #region TestDictionaryCorrection [C:3] [TYPE Module] [SEMANTICS test, dictionary, correction, context]\n# @BRIEF Validate InlineCorrectionService and dictionary correction flows.\n# @RELATION BINDS_TO -> [InlineCorrectionService]\n\nimport pytest\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import (\n Base,\n DictionaryEntry,\n TranslationBatch,\n TranslationJob,\n TranslationLanguage,\n TranslationRecord,\n TranslationRun,\n)\nfrom src.plugins.translate.dictionary import DictionaryManager\nfrom src.plugins.translate.service import InlineCorrectionService\n\n\n# region db_session [TYPE Fixture]\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestContextCapture:\n \"\"\"Verify context auto-capture when submitting corrections.\"\"\"\n\n # region test_context_capture_in_correction [C:2] [TYPE Function]\n def test_context_capture_in_correction(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test Dict\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Ctx Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"DRAFT\")\n db_session.add(job)\n db_session.flush()\n\n run = TranslationRun(job_id=job.id, status=\"COMPLETED\")\n db_session.add(run)\n db_session.flush()\n\n batch = TranslationBatch(run_id=run.id, batch_index=0, status=\"COMPLETED\")\n db_session.add(batch)\n db_session.flush()\n\n record = TranslationRecord(\n batch_id=batch.id, run_id=run.id, source_sql=\"hello world\",\n source_object_type=\"table_row\", source_object_name=\"Row 0\",\n source_object_id=\"0\", source_data={\"schema\": \"public\", \"table\": \"users\", \"column\": \"name\"},\n status=\"SUCCESS\",\n )\n db_session.add(record)\n db_session.flush()\n\n for lang_code in (\"ru\", \"de\", \"fr\", \"it\"):\n tl = TranslationLanguage(\n record_id=record.id, language_code=lang_code,\n source_language_detected=\"en\", translated_value=\"translated\",\n final_value=\"translated\", status=\"translated\",\n )\n db_session.add(tl)\n db_session.commit()\n\n result = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"ru\",\n dictionary_id=d.id, corrected_value=\"привет мир (исправлено)\",\n current_user=\"test_user\",\n )\n assert result[\"action\"] == \"created\"\n assert result[\"entry_id\"] is not None\n\n entry = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result[\"entry_id\"]).first()\n assert entry is not None\n assert entry.has_context is True\n assert entry.context_source == \"auto\"\n assert entry.context_data is not None\n assert \"source_data\" in entry.context_data\n assert entry.context_data[\"source_object_type\"] == \"table_row\"\n\n result2 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"de\",\n dictionary_id=d.id, corrected_value=\"another correction\",\n current_user=\"test_user\", context_data_override={\"custom_key\": \"custom_value\"},\n )\n assert result2[\"action\"] == \"created\"\n entry2 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result2[\"entry_id\"]).first()\n assert entry2.context_data == {\"custom_key\": \"custom_value\"}\n assert entry2.context_source == \"manual\"\n\n result3 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"fr\",\n dictionary_id=d.id, corrected_value=\"no context\",\n current_user=\"test_user\", keep_context=False,\n )\n assert result3[\"action\"] == \"created\"\n entry3 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result3[\"entry_id\"]).first()\n assert entry3.context_data is None\n assert entry3.has_context is False\n assert entry3.context_source == \"manual\"\n\n result4 = InlineCorrectionService.submit_correction_to_dict(\n db=db_session, record_id=record.id, language_code=\"it\",\n dictionary_id=d.id, corrected_value=\"with notes\",\n current_user=\"test_user\", usage_notes=\"Use this for user-facing columns only\",\n )\n assert result4[\"action\"] == \"created\"\n entry4 = db_session.query(DictionaryEntry).filter(DictionaryEntry.id == result4[\"entry_id\"]).first()\n assert entry4.usage_notes == \"Use this for user-facing columns only\"\n # endregion test_context_capture_in_correction\n# #endregion TestDictionaryCorrection\n" }, { "contract_id": "TestDictionaryCRUD", @@ -36292,7 +37153,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDictionaryCRUD [C:3] [TYPE Module] [SEMANTICS test, dictionary, crud]\n# @BRIEF Validate DictionaryCRUD and DictionaryEntryCRUD operations.\n# @RELATION BINDS_TO -> [DictionaryCRUD]\n# @RELATION BINDS_TO -> [DictionaryEntryCRUD]\n# @TEST_EDGE duplicate_entry -> ValueError on repeated (dictionary_id, source_term_norm, source_lang, target_lang)\n# @TEST_EDGE delete_active_job -> ValueError with active/scheduled message\n# @TEST_EDGE same_term_different_lang_pair -> allowed (not duplicate)\n# @TEST_INVARIANT unique_normalized -> verifies: [duplicate_entry, same_term_different_lang_pair allowed]\n\nimport pytest\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import Base, DictionaryEntry, TerminologyDictionary, TranslationJob, TranslationJobDictionary\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n\n# region db_session [TYPE Fixture]\n# @PURPOSE: Provide an in-memory SQLite session for each test.\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestDictionaryCRUD:\n \"\"\"Verify dictionary-level CRUD operations.\"\"\"\n\n # region test_create_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary creation and read-back.\n def test_create_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Finance Terms\",\n source_dialect=\"postgresql\", target_dialect=\"clickhouse\",\n created_by=\"test_user\", description=\"Finance-related term mappings\",\n )\n assert d.id is not None\n assert d.name == \"Finance Terms\"\n assert d.source_dialect == \"postgresql\"\n assert d.target_dialect == \"clickhouse\"\n assert d.created_by == \"test_user\"\n assert d.is_active is True\n\n fetched = DictionaryManager.get_dictionary(db_session, d.id)\n assert fetched.id == d.id\n assert fetched.name == \"Finance Terms\"\n # endregion test_create_dictionary\n\n # region test_update_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary metadata update.\n def test_update_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Old Name\", source_dialect=\"a\", target_dialect=\"b\")\n updated = DictionaryManager.update_dictionary(\n db_session, d.id, name=\"New Name\", description=\"Updated desc\", is_active=False,\n )\n assert updated.name == \"New Name\"\n assert updated.description == \"Updated desc\"\n assert updated.is_active is False\n # endregion test_update_dictionary\n\n # region test_delete_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary deletion also removes entries.\n def test_delete_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"To Delete\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary\n\n # region test_list_dictionaries [C:2] [TYPE Function]\n # @BRIEF Verify paginated dictionary listing.\n def test_list_dictionaries(self, db_session: Session):\n for i in range(5):\n DictionaryManager.create_dictionary(db_session, name=f\"Dict {i}\", source_dialect=\"a\", target_dialect=\"b\")\n dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)\n assert total == 5\n assert len(dicts) == 2\n # endregion test_list_dictionaries\n\n # region test_delete_dictionary_blocked_by_active_job [C:2] [TYPE Function]\n # @BRIEF Verify deletion is blocked when attached to active/scheduled jobs.\n def test_delete_dictionary_blocked_by_active_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Active Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"ACTIVE\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n with pytest.raises(ValueError, match=\"active/scheduled\"):\n DictionaryManager.delete_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_blocked_by_active_job\n\n # region test_delete_dictionary_allowed_with_completed_job [C:2] [TYPE Function]\n # @BRIEF Verify deletion is allowed when only completed/failed jobs reference the dictionary.\n def test_delete_dictionary_allowed_with_completed_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Completed Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"COMPLETED\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_allowed_with_completed_job\n\n\nclass TestDictionaryEntryCRUD:\n \"\"\"Verify entry-level CRUD operations.\"\"\"\n\n # region test_add_entry_duplicate [C:2] [TYPE Function]\n # @BRIEF Verify duplicate entry raises ValueError.\n def test_add_entry_duplicate(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"Hello\", \"Hola\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"Bonjour\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"HELLO\", \"Ciao\", source_language=\"en\", target_language=\"es\")\n # endregion test_add_entry_duplicate\n\n # region test_add_entry_duplicate_per_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify duplicate is per-dictionary (same term in different dicts is OK).\n def test_add_entry_duplicate_per_dictionary(self, db_session: Session):\n d1 = DictionaryManager.create_dictionary(db_session, name=\"Dict1\", source_dialect=\"a\", target_dialect=\"b\")\n d2 = DictionaryManager.create_dictionary(db_session, name=\"Dict2\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n entry = DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n # endregion test_add_entry_duplicate_per_dictionary\n\n # region test_edit_entry [C:2] [TYPE Function]\n # @BRIEF Verify entry edit updates fields and enforces uniqueness.\n def test_edit_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n updated = DictionaryManager.edit_entry(db_session, entry.id, target_term=\"HOLA!\")\n assert updated.target_term == \"HOLA!\"\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.edit_entry(db_session, entry.id, source_term=\"WORLD\")\n # endregion test_edit_entry\n\n # region test_delete_entry [C:2] [TYPE Function]\n # @BRIEF Verify entry deletion.\n def test_delete_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.delete_entry(db_session, entry.id)\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_delete_entry\n\n # region test_clear_entries [C:2] [TYPE Function]\n # @BRIEF Verify clearing all entries for a dictionary.\n def test_clear_entries(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n deleted = DictionaryManager.clear_entries(db_session, d.id)\n assert deleted == 2\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_clear_entries\n\n # region test_add_entry_with_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify creating entry with language pair stores correctly.\n def test_add_entry_with_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Lang Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n assert entry.source_language == \"en\"\n assert entry.target_language == \"ru\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1\n assert entries[0].source_language == \"en\"\n assert entries[0].target_language == \"ru\"\n # endregion test_add_entry_with_language_pair\n\n # region test_duplicate_same_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify duplicate with same language pair raises conflict.\n def test_duplicate_same_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Dup Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"ru\")\n # endregion test_duplicate_same_language_pair\n\n # region test_same_term_different_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify same term with different language pair is allowed.\n def test_same_term_different_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Multi Lang\", source_dialect=\"a\", target_dialect=\"b\")\n entry1 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n entry2 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"de\")\n assert entry1.id != entry2.id\n assert entry1.target_language == \"ru\"\n assert entry2.target_language == \"de\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 2\n # endregion test_same_term_different_language_pair\n# #endregion TestDictionaryCRUD\n" + "body": "# #region TestDictionaryCRUD [C:3] [TYPE Module] [SEMANTICS test, dictionary, crud]\n# @BRIEF Validate DictionaryCRUD and DictionaryEntryCRUD operations.\n# @RELATION BINDS_TO -> [DictionaryCRUD]\n# @RELATION BINDS_TO -> [DictionaryEntryCRUD]\n# @TEST_EDGE duplicate_entry -> ValueError on repeated (dictionary_id, source_term_norm, source_lang, target_lang)\n# @TEST_EDGE delete_active_job -> ValueError with active/scheduled message\n# @TEST_EDGE same_term_different_lang_pair -> allowed (not duplicate)\n# @TEST_INVARIANT unique_normalized -> verifies: [duplicate_entry, same_term_different_lang_pair allowed]\n\nimport pytest\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import Session, sessionmaker\n\nfrom src.models.translate import Base, TranslationJob, TranslationJobDictionary\nfrom src.plugins.translate.dictionary import DictionaryManager\n\n\n# region db_session [TYPE Fixture]\n# @PURPOSE: Provide an in-memory SQLite session for each test.\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n\n @event.listens_for(engine, \"connect\")\n def _set_sqlite_pragma(dbapi_connection, connection_record):\n cursor = dbapi_connection.cursor()\n cursor.execute(\"PRAGMA foreign_keys=ON\")\n cursor.close()\n\n Base.metadata.create_all(bind=engine)\n session = sessionmaker(bind=engine)()\n try:\n yield session\n finally:\n session.close()\n Base.metadata.drop_all(bind=engine)\n# endregion db_session\n\n\nclass TestDictionaryCRUD:\n \"\"\"Verify dictionary-level CRUD operations.\"\"\"\n\n # region test_create_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary creation and read-back.\n def test_create_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(\n db_session, name=\"Finance Terms\",\n source_dialect=\"postgresql\", target_dialect=\"clickhouse\",\n created_by=\"test_user\", description=\"Finance-related term mappings\",\n )\n assert d.id is not None\n assert d.name == \"Finance Terms\"\n assert d.source_dialect == \"postgresql\"\n assert d.target_dialect == \"clickhouse\"\n assert d.created_by == \"test_user\"\n assert d.is_active is True\n\n fetched = DictionaryManager.get_dictionary(db_session, d.id)\n assert fetched.id == d.id\n assert fetched.name == \"Finance Terms\"\n # endregion test_create_dictionary\n\n # region test_update_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary metadata update.\n def test_update_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Old Name\", source_dialect=\"a\", target_dialect=\"b\")\n updated = DictionaryManager.update_dictionary(\n db_session, d.id, name=\"New Name\", description=\"Updated desc\", is_active=False,\n )\n assert updated.name == \"New Name\"\n assert updated.description == \"Updated desc\"\n assert updated.is_active is False\n # endregion test_update_dictionary\n\n # region test_delete_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify dictionary deletion also removes entries.\n def test_delete_dictionary(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"To Delete\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary\n\n # region test_list_dictionaries [C:2] [TYPE Function]\n # @BRIEF Verify paginated dictionary listing.\n def test_list_dictionaries(self, db_session: Session):\n for i in range(5):\n DictionaryManager.create_dictionary(db_session, name=f\"Dict {i}\", source_dialect=\"a\", target_dialect=\"b\")\n dicts, total = DictionaryManager.list_dictionaries(db_session, page=1, page_size=2)\n assert total == 5\n assert len(dicts) == 2\n # endregion test_list_dictionaries\n\n # region test_delete_dictionary_blocked_by_active_job [C:2] [TYPE Function]\n # @BRIEF Verify deletion is blocked when attached to active/scheduled jobs.\n def test_delete_dictionary_blocked_by_active_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Active Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"ACTIVE\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n with pytest.raises(ValueError, match=\"active/scheduled\"):\n DictionaryManager.delete_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_blocked_by_active_job\n\n # region test_delete_dictionary_allowed_with_completed_job [C:2] [TYPE Function]\n # @BRIEF Verify deletion is allowed when only completed/failed jobs reference the dictionary.\n def test_delete_dictionary_allowed_with_completed_job(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n job = TranslationJob(name=\"Completed Job\", source_dialect=\"a\", target_dialect=\"b\", status=\"COMPLETED\", created_by=\"test_user\")\n db_session.add(job)\n db_session.flush()\n link = TranslationJobDictionary(job_id=job.id, dictionary_id=d.id)\n db_session.add(link)\n db_session.commit()\n DictionaryManager.delete_dictionary(db_session, d.id)\n with pytest.raises(ValueError, match=\"Dictionary not found\"):\n DictionaryManager.get_dictionary(db_session, d.id)\n # endregion test_delete_dictionary_allowed_with_completed_job\n\n\nclass TestDictionaryEntryCRUD:\n \"\"\"Verify entry-level CRUD operations.\"\"\"\n\n # region test_add_entry_duplicate [C:2] [TYPE Function]\n # @BRIEF Verify duplicate entry raises ValueError.\n def test_add_entry_duplicate(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"Hello\", \"Hola\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"Bonjour\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"HELLO\", \"Ciao\", source_language=\"en\", target_language=\"es\")\n # endregion test_add_entry_duplicate\n\n # region test_add_entry_duplicate_per_dictionary [C:2] [TYPE Function]\n # @BRIEF Verify duplicate is per-dictionary (same term in different dicts is OK).\n def test_add_entry_duplicate_per_dictionary(self, db_session: Session):\n d1 = DictionaryManager.create_dictionary(db_session, name=\"Dict1\", source_dialect=\"a\", target_dialect=\"b\")\n d2 = DictionaryManager.create_dictionary(db_session, name=\"Dict2\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d1.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n entry = DictionaryManager.add_entry(db_session, d2.id, \"hello\", \"bonjour\", source_language=\"en\", target_language=\"es\")\n assert entry.id is not None\n # endregion test_add_entry_duplicate_per_dictionary\n\n # region test_edit_entry [C:2] [TYPE Function]\n # @BRIEF Verify entry edit updates fields and enforces uniqueness.\n def test_edit_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n updated = DictionaryManager.edit_entry(db_session, entry.id, target_term=\"HOLA!\")\n assert updated.target_term == \"HOLA!\"\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.edit_entry(db_session, entry.id, source_term=\"WORLD\")\n # endregion test_edit_entry\n\n # region test_delete_entry [C:2] [TYPE Function]\n # @BRIEF Verify entry deletion.\n def test_delete_entry(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.delete_entry(db_session, entry.id)\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_delete_entry\n\n # region test_clear_entries [C:2] [TYPE Function]\n # @BRIEF Verify clearing all entries for a dictionary.\n def test_clear_entries(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hola\", source_language=\"en\", target_language=\"es\")\n DictionaryManager.add_entry(db_session, d.id, \"world\", \"mundo\", source_language=\"en\", target_language=\"es\")\n deleted = DictionaryManager.clear_entries(db_session, d.id)\n assert deleted == 2\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 0\n # endregion test_clear_entries\n\n # region test_add_entry_with_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify creating entry with language pair stores correctly.\n def test_add_entry_with_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Lang Test\", source_dialect=\"a\", target_dialect=\"b\")\n entry = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n assert entry.source_language == \"en\"\n assert entry.target_language == \"ru\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 1\n assert entries[0].source_language == \"en\"\n assert entries[0].target_language == \"ru\"\n # endregion test_add_entry_with_language_pair\n\n # region test_duplicate_same_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify duplicate with same language pair raises conflict.\n def test_duplicate_same_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Dup Test\", source_dialect=\"a\", target_dialect=\"b\")\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n with pytest.raises(ValueError, match=\"already exists\"):\n DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"ru\")\n # endregion test_duplicate_same_language_pair\n\n # region test_same_term_different_language_pair [C:2] [TYPE Function]\n # @BRIEF Verify same term with different language pair is allowed.\n def test_same_term_different_language_pair(self, db_session: Session):\n d = DictionaryManager.create_dictionary(db_session, name=\"Multi Lang\", source_dialect=\"a\", target_dialect=\"b\")\n entry1 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"привет\", source_language=\"en\", target_language=\"ru\")\n entry2 = DictionaryManager.add_entry(db_session, d.id, \"hello\", \"hallo\", source_language=\"en\", target_language=\"de\")\n assert entry1.id != entry2.id\n assert entry1.target_language == \"ru\"\n assert entry2.target_language == \"de\"\n entries, total = DictionaryManager.list_entries(db_session, d.id)\n assert total == 2\n # endregion test_same_term_different_language_pair\n# #endregion TestDictionaryCRUD\n" }, { "contract_id": "TestDictionaryFilter", @@ -36598,7 +37459,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/_batch_insert.py", "start_line": 1, - "end_line": 233, + "end_line": 232, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36609,14 +37470,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region BatchInsertService [C:3] [TYPE Module] [SEMANTICS translate, insert, sqllab, upsert, target]\n# @BRIEF Insert successful translation records into target table via Superset SQL Lab.\n# Builds INSERT SQL (original + per-language rows), resolves backend dialect,\n# and executes via SupersetSqlLabExecutor.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [SQLGenerator], [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Batch has committed TranslationRecords with status SUCCESS. Job has target_table configured.\n# @POST Per record, N+1 rows are INSERTED: 1 original + N translations.\n# @SIDE_EFFECT HTTP call to Superset SQL Lab API. Writes to target database.\n# @RATIONALE Extracted from BatchProcessingService (583 lines) to comply with INV_7.\n# @REJECTED Inline insert logic in process_batch — made the function 583 lines.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import SQLGenerator, _normalize_timestamp_value\nfrom .superset_executor import SupersetSqlLabExecutor\n\n\n# #region insert_batch_to_target [C:3] [TYPE Function] [SEMANTICS translate, insert, orchestrate]\n# @BRIEF Insert successful records from a single batch into the target table.\ndef insert_batch_to_target(\n db: Session, config_manager: ConfigManager,\n job: TranslationJob, batch_id: str, run_id: str,\n) -> None:\n \"\"\"Insert successful batch records into the target table via Superset SQL Lab.\"\"\"\n with belief_scope(\"BatchInsertService.insert_batch_to_target\"):\n records = _fetch_batch_records(db, batch_id)\n if not records:\n return\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n columns = _build_target_columns(job, effective_target)\n context_keys = _build_context_keys(job, effective_target)\n rows_for_sql = _build_insert_rows(records, job, effective_target, primary_language, context_keys)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n dialect, executor = _resolve_insert_backend(config_manager, job, batch_id)\n if dialect is None:\n return\n\n sql, row_count = _generate_insert_sql(dialect, job, columns, rows_for_sql, batch_id)\n if sql is None:\n return\n\n _execute_insert_sql(executor, sql, batch_id, row_count)\n# #endregion insert_batch_to_target\n\n\n# #region _fetch_batch_records [C:1] [TYPE Function]\ndef _fetch_batch_records(db: Session, batch_id: str) -> list[TranslationRecord]:\n \"\"\"Fetch successful TranslationRecords for a batch with languages.\"\"\"\n return (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.batch_id == batch_id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n# #endregion _fetch_batch_records\n\n\n# #region _build_target_columns [C:1] [TYPE Function]\ndef _build_target_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build the list of column names for the target INSERT.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion _build_target_columns\n\n\n# #region _build_context_keys [C:1] [TYPE Function]\ndef _build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context keys for JSON bundled column.\"\"\"\n context_keys = list(job.context_columns or [])\n if (\n job.translation_column\n and job.translation_column != effective_target\n and job.translation_column not in context_keys\n ):\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion _build_context_keys\n\n\n# #region _build_insert_rows [C:3] [TYPE Function]\ndef _build_insert_rows(\n records: list[TranslationRecord], job: TranslationJob,\n effective_target: str | None, primary_language: str, context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row dicts for the INSERT SQL statement.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback = dict(base_row)\n if effective_target:\n fallback[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback[job.target_language_column] = primary_language\n fallback[\"is_original\"] = 0\n rows_for_sql.append(fallback)\n return rows_for_sql\n# #endregion _build_insert_rows\n\n\n# #region _resolve_insert_backend [C:1] [TYPE Function]\ndef _resolve_insert_backend(\n config_manager: ConfigManager, job: TranslationJob, batch_id: str,\n) -> tuple[str | None, SupersetSqlLabExecutor | None]:\n \"\"\"Resolve the database backend and SQL executor for batch insert.\"\"\"\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend for batch insert\",\n {\"batch_id\": batch_id, \"error\": str(e)})\n return None, None\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n return dialect, executor\n# #endregion _resolve_insert_backend\n\n\n# #region _generate_insert_sql [C:1] [TYPE Function]\ndef _generate_insert_sql(\n dialect: str, job: TranslationJob, columns: list[str],\n rows_for_sql: list[dict[str, object]], batch_id: str = \"\",\n) -> tuple[str | None, int]:\n \"\"\"Generate INSERT SQL using SQLGenerator.\"\"\"\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect, target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns, rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n return sql, row_count\n except ValueError as e:\n logger.explore(\"SQL generation failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return None, 0\n# #endregion _generate_insert_sql\n\n\n# #region _execute_insert_sql [C:1] [TYPE Function]\ndef _execute_insert_sql(executor: SupersetSqlLabExecutor, sql: str, batch_id: str, row_count: int) -> None:\n \"\"\"Execute the INSERT SQL via Superset SQL Lab.\"\"\"\n try:\n result = executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)\n except Exception as e:\n logger.explore(\"Superset SQL submission failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return\n logger.reason(f\"Batch {batch_id[:12]} inserted {row_count} rows\",\n {\"batch_id\": batch_id, \"rows\": row_count, \"status\": result.get(\"status\")})\n# #endregion _execute_insert_sql\n# #endregion BatchInsertService\n" + "body": "# #region BatchInsertService [C:3] [TYPE Module] [SEMANTICS translate, insert, sqllab, upsert, target]\n# @BRIEF Insert successful translation records into target table via Superset SQL Lab.\n# Builds INSERT SQL (original + per-language rows), resolves backend dialect,\n# and executes via SupersetSqlLabExecutor.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [SQLGenerator], [SupersetSqlLabExecutor]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @PRE Batch has committed TranslationRecords with status SUCCESS. Job has target_table configured.\n# @POST Per record, N+1 rows are INSERTED: 1 original + N translations.\n# @SIDE_EFFECT HTTP call to Superset SQL Lab API. Writes to target database.\n# @RATIONALE Extracted from BatchProcessingService (583 lines) to comply with INV_7.\n# @REJECTED Inline insert logic in process_batch — made the function 583 lines.\n\nimport json\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import SQLGenerator, _normalize_timestamp_value\nfrom .superset_executor import SupersetSqlLabExecutor\n\n\n# #region insert_batch_to_target [C:3] [TYPE Function] [SEMANTICS translate, insert, orchestrate]\n# @BRIEF Insert successful records from a single batch into the target table.\ndef insert_batch_to_target(\n db: Session, config_manager: ConfigManager,\n job: TranslationJob, batch_id: str, run_id: str,\n) -> None:\n \"\"\"Insert successful batch records into the target table via Superset SQL Lab.\"\"\"\n with belief_scope(\"BatchInsertService.insert_batch_to_target\"):\n records = _fetch_batch_records(db, batch_id)\n if not records:\n return\n\n effective_target = job.target_column or job.translation_column\n primary_language = (job.target_languages or [\"en\"])[0]\n columns = _build_target_columns(job, effective_target)\n context_keys = _build_context_keys(job, effective_target)\n rows_for_sql = _build_insert_rows(records, job, effective_target, primary_language, context_keys)\n\n if not columns:\n columns = [effective_target or \"translated_text\"]\n rows_for_sql = [{columns[0]: rec.target_sql or \"\"} for rec in records]\n\n dialect, executor = _resolve_insert_backend(config_manager, job, batch_id)\n if dialect is None:\n return\n\n sql, row_count = _generate_insert_sql(dialect, job, columns, rows_for_sql, batch_id)\n if sql is None:\n return\n\n _execute_insert_sql(executor, sql, batch_id, row_count)\n# #endregion insert_batch_to_target\n\n\n# #region _fetch_batch_records [C:1] [TYPE Function]\ndef _fetch_batch_records(db: Session, batch_id: str) -> list[TranslationRecord]:\n \"\"\"Fetch successful TranslationRecords for a batch with languages.\"\"\"\n return (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.batch_id == batch_id,\n TranslationRecord.status == \"SUCCESS\",\n TranslationRecord.target_sql.isnot(None),\n )\n .all()\n )\n# #endregion _fetch_batch_records\n\n\n# #region _build_target_columns [C:1] [TYPE Function]\ndef _build_target_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build the list of column names for the target INSERT.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion _build_target_columns\n\n\n# #region _build_context_keys [C:1] [TYPE Function]\ndef _build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context keys for JSON bundled column.\"\"\"\n context_keys = list(job.context_columns or [])\n if (\n job.translation_column\n and job.translation_column != effective_target\n and job.translation_column not in context_keys\n ):\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion _build_context_keys\n\n\n# #region _build_insert_rows [C:3] [TYPE Function]\ndef _build_insert_rows(\n records: list[TranslationRecord], job: TranslationJob,\n effective_target: str | None, primary_language: str, context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row dicts for the INSERT SQL statement.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data: dict[str, str] = {}\n for key in context_keys:\n val = source_data.get(key)\n context_data[key] = str(val) if val is not None else \"\"\n\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback = dict(base_row)\n if effective_target:\n fallback[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback[job.target_language_column] = primary_language\n fallback[\"is_original\"] = 0\n rows_for_sql.append(fallback)\n return rows_for_sql\n# #endregion _build_insert_rows\n\n\n# #region _resolve_insert_backend [C:1] [TYPE Function]\ndef _resolve_insert_backend(\n config_manager: ConfigManager, job: TranslationJob, batch_id: str,\n) -> tuple[str | None, SupersetSqlLabExecutor | None]:\n \"\"\"Resolve the database backend and SQL executor for batch insert.\"\"\"\n try:\n env_id = job.environment_id or job.source_dialect or \"\"\n executor = SupersetSqlLabExecutor(config_manager, env_id)\n executor.resolve_database_id(target_database_id=job.target_database_id)\n real_backend = executor.get_database_backend()\n except Exception as e:\n logger.explore(\"Failed to resolve database backend for batch insert\",\n {\"batch_id\": batch_id, \"error\": str(e)})\n return None, None\n dialect = real_backend or job.database_dialect or job.target_dialect or \"postgresql\"\n return dialect, executor\n# #endregion _resolve_insert_backend\n\n\n# #region _generate_insert_sql [C:1] [TYPE Function]\ndef _generate_insert_sql(\n dialect: str, job: TranslationJob, columns: list[str],\n rows_for_sql: list[dict[str, object]], batch_id: str = \"\",\n) -> tuple[str | None, int]:\n \"\"\"Generate INSERT SQL using SQLGenerator.\"\"\"\n try:\n sql, row_count = SQLGenerator.generate(\n dialect=dialect, target_schema=job.target_schema,\n target_table=job.target_table or \"translated_data\",\n columns=columns, rows=rows_for_sql,\n key_columns=job.target_key_cols,\n upsert_strategy=job.upsert_strategy or \"MERGE\",\n )\n return sql, row_count\n except ValueError as e:\n logger.explore(\"SQL generation failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return None, 0\n# #endregion _generate_insert_sql\n\n\n# #region _execute_insert_sql [C:1] [TYPE Function]\ndef _execute_insert_sql(executor: SupersetSqlLabExecutor, sql: str, batch_id: str, row_count: int) -> None:\n \"\"\"Execute the INSERT SQL via Superset SQL Lab.\"\"\"\n try:\n result = executor.execute_and_poll(sql=sql, max_polls=30, poll_interval_seconds=2.0)\n except Exception as e:\n logger.explore(\"Superset SQL submission failed for batch\", {\"batch_id\": batch_id, \"error\": str(e)})\n return\n logger.reason(f\"Batch {batch_id[:12]} inserted {row_count} rows\",\n {\"batch_id\": batch_id, \"rows\": row_count, \"status\": result.get(\"status\")})\n# #endregion _execute_insert_sql\n# #endregion BatchInsertService\n" }, { "contract_id": "insert_batch_to_target", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 27, - "end_line": 58, + "start_line": 26, + "end_line": 57, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36633,8 +37494,8 @@ "contract_id": "_fetch_batch_records", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 61, - "end_line": 74, + "start_line": 60, + "end_line": 73, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36650,8 +37511,8 @@ "contract_id": "_build_target_columns", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 77, - "end_line": 100, + "start_line": 76, + "end_line": 99, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36667,8 +37528,8 @@ "contract_id": "_build_context_keys", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 103, - "end_line": 114, + "start_line": 102, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36684,8 +37545,8 @@ "contract_id": "_build_insert_rows", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 117, - "end_line": 179, + "start_line": 116, + "end_line": 178, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36701,8 +37562,8 @@ "contract_id": "_resolve_insert_backend", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 182, - "end_line": 198, + "start_line": 181, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36718,8 +37579,8 @@ "contract_id": "_generate_insert_sql", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 201, - "end_line": 219, + "start_line": 200, + "end_line": 218, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36735,8 +37596,8 @@ "contract_id": "_execute_insert_sql", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_insert.py", - "start_line": 222, - "end_line": 232, + "start_line": 221, + "end_line": 231, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -36764,7 +37625,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region BatchProcessingService [C:4] [TYPE Module] [SEMANTICS translate, batch, process, classify, cache]\n# @BRIEF Batch processing for translation: classify rows (same-language/cache/preview/LLM),\n# call LLM service, persist TranslationRecord/TranslationLanguage rows.\n# Local language detection (lingua) replaces LLM-based detection.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [DictionaryManager], [LLMTranslationService], [LanguageDetectService]\n# @RELATION DEPENDS_ON -> [estimate_token_budget], [ConfigManager]\n# @PRE DB session is available. Job configuration is valid.\n# @POST TranslationBatch, TranslationRecord, TranslationLanguage rows created and committed.\n# @SIDE_EFFECT LLM API calls via LLMTranslationService; DB writes.\n# @RATIONALE Extracted from TranslationExecutor. Batch insert delegated to _batch_insert.py.\n# @REJECTED Keeping batch processing inside TranslationExecutor — caused class to exceed INV_7.\n\nfrom datetime import UTC, datetime\nimport time\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationBatch, TranslationJob, TranslationLanguage, TranslationRecord\nfrom ...services.llm_provider import LLMProviderService\nfrom ._batch_insert import insert_batch_to_target\nfrom ._lang_detect import batch_detect, get_detector\nfrom ._llm_call import LLMTranslationService\nfrom ._token_budget import estimate_token_budget\nfrom ._utils import _check_translation_cache, _compute_key_hash, _compute_source_hash\nfrom .dictionary import DictionaryManager\n\n\n# #region BatchProcessingService [C:4] [TYPE Class]\n# @BRIEF Create batch records, classify rows, process LLM calls, persist results.\nclass BatchProcessingService:\n \"\"\"Process a batch: classify (cache/preview/LLM), persist, and insert to target.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager) -> None:\n self.db = db\n self.config_manager = config_manager\n self._llm_service = LLMTranslationService(db)\n\n # #region process_batch [C:3] [TYPE Function]\n # @BRIEF Process a single batch: create record, classify rows, call LLM, persist.\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, job: TranslationJob, run_id: str, batch_index: int,\n batch_rows: list[dict[str, Any]],\n dict_snapshot_hash: str | None = None, config_hash: str | None = None,\n preview_edits_cache: dict[str, dict[str, str]] | None = None,\n ) -> dict[str, int]:\n \"\"\"Process a single batch: classify rows, call LLM (if needed), persist records.\"\"\"\n with belief_scope(\"BatchProcessingService.process_batch\"):\n batch_start = time.monotonic()\n batch = self._create_batch(run_id, batch_index, batch_rows)\n bid = batch.id\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n tls = job.target_languages or [job.target_dialect or \"en\"]\n tls = [str(tls)] if not isinstance(tls, list) else tls\n\n # ★ Run local language detection on all rows (heuristic, no LLM)\n self._detect_languages(batch_rows, tls)\n\n source_texts = [r.get(\"source_text\", \"\") for r in batch_rows if r.get(\"source_text\")]\n rc = batch_rows[0].get(\"source_data\") if batch_rows else None\n dict_matches = DictionaryManager.filter_for_batch(self.db, source_texts, job.id, row_context=rc)\n\n self._check_cache(job, batch_rows, dict_snapshot_hash, config_hash)\n llm_rows, pre_rows = self._classify(batch_rows, preview_edits_cache, tls)\n\n result[\"successful\"] += self._persist_pre(pre_rows, bid, run_id, tls)\n if llm_rows:\n llm_res = self._process_llm(job, run_id, llm_rows, dict_matches, bid, tls)\n for k in (\"successful\", \"failed\", \"skipped\", \"retries\"):\n result[k] += llm_res.get(k, 0)\n\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(UTC)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n latency = int((time.monotonic() - batch_start) * 1000)\n logger.reason(f\"Batch {batch_index} complete\", {\"batch_id\": bid, \"latency_ms\": latency, **result})\n return {**result, \"batch_id\": bid}\n # #endregion process_batch\n\n def _create_batch(self, run_id, batch_index, batch_rows):\n b = TranslationBatch(id=str(uuid.uuid4()), run_id=run_id, batch_index=batch_index,\n status=\"RUNNING\", total_records=len(batch_rows), started_at=datetime.now(UTC))\n self.db.add(b)\n self.db.flush()\n return b\n\n def _check_cache(self, job, batch_rows, dict_snapshot_hash, config_hash):\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n continue\n st = row.get(\"source_text\", \"\")\n if not st:\n continue\n ctx = list(job.context_columns or [])\n h = _compute_source_hash(st, row.get(\"source_data\"), dict_snapshot_hash, config_hash, ctx)\n row[\"_source_hash\"] = h\n cached = _check_translation_cache(self.db, h)\n if cached:\n row[\"_cached_lang_values\"] = cached\n logger.reason(\"Translation cache hit\", {\"source_hash\": h[:12], \"langs\": list(cached.keys())})\n\n # ★ Local language detection — replaces LLM-based detection\n def _detect_languages(self, batch_rows: list[dict], target_languages: list[str]) -> None:\n \"\"\"Run local language detection on all batch rows (no LLM).\n\n Attaches '_detected_lang' (BCP-47 code or 'und') to each row dict.\n Uses batch_detect() for efficient multi-text processing.\n \"\"\"\n texts = [row.get(\"source_text\", \"\") for row in batch_rows]\n results = batch_detect(texts, target_languages)\n for row, lang in zip(batch_rows, results):\n row[\"_detected_lang\"] = lang\n\n def _classify(self, batch_rows, preview_edits_cache, tls):\n llm_rows, pre_rows = [], []\n tls_lower = [str(t).lower() for t in tls]\n for row in batch_rows:\n # ★ NEW: Same-language pre-filter — detected language matches target\n dl = row.get(\"_detected_lang\")\n if dl and str(dl).lower() not in (\"und\", \"\") and str(dl).lower() in tls_lower:\n row[\"_same_language\"] = True\n pre_rows.append(row)\n continue\n\n if row.get(\"approved_translation\"):\n pre_rows.append(row)\n continue\n cl = row.get(\"_cached_lang_values\")\n if cl:\n if all(lc in cl for lc in tls):\n pre_rows.append(row)\n continue\n if preview_edits_cache:\n sd = row.get(\"source_data\") or {}\n if sd:\n kh = _compute_key_hash(sd)\n pe = preview_edits_cache.get(kh)\n if pe:\n fe = next(iter(pe.values()), None)\n if fe:\n row[\"approved_translation\"] = fe\n pre_rows.append(row)\n continue\n llm_rows.append(row)\n return llm_rows, pre_rows\n\n def _persist_pre(self, pre_rows, bid, run_id, tls):\n count = 0\n for row in pre_rows:\n # ★ Same-language row: source language = target → no translation needed\n if row.get(\"_same_language\"):\n dl = str(row[\"_detected_lang\"])\n source_text = row.get(\"source_text\", \"\")\n rec = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=bid, run_id=run_id,\n source_sql=source_text, target_sql=source_text,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(rec)\n # One TranslationLanguage for the detected (same) language\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=rec.id, language_code=dl,\n source_language_detected=dl, translated_value=source_text,\n final_value=source_text, status=\"translated\", needs_review=False,\n ))\n count += 1\n continue\n\n # Existing: cached / approved rows\n cl = row.get(\"_cached_lang_values\")\n detected_lang = row.get(\"_detected_lang\", \"und\") or \"und\"\n rec = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=bid, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=row.get(\"approved_translation\", \"\"),\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(rec)\n for lc in tls:\n fv = cl[lc] if (cl and lc in cl) else row.get(\"approved_translation\", \"\")\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=rec.id, language_code=lc,\n source_language_detected=detected_lang, translated_value=fv,\n final_value=fv, status=\"translated\", needs_review=False,\n ))\n count += 1\n return count\n\n def _process_llm(self, job, run_id, rows_for_llm, dict_matches, bid, tls):\n provider_model = None\n if job.provider_id:\n try:\n p = LLMProviderService(self.db).get_provider(job.provider_id)\n if p:\n provider_model = p.default_model or \"gpt-4o-mini\"\n except Exception:\n provider_model = None\n\n tb = estimate_token_budget(\n source_rows=rows_for_llm, target_languages=tls,\n source_column=\"source_text\", context_columns=None,\n dictionary_entries=dict_matches, batch_size=len(rows_for_llm),\n provider_info=provider_model,\n )\n if tb[\"warning\"]:\n logger.explore(\"Token budget warning\", {\"batch_id\": bid, \"warning\": tb[\"warning\"]})\n\n return self._llm_service.call_llm_for_batch(\n job=job, run_id=run_id, batch_rows=rows_for_llm,\n dict_matches=dict_matches, batch_id=bid,\n max_tokens=tb[\"max_output_needed\"],\n )\n\n # -- Batch insert (delegation) --\n def insert_batch_to_target(self, job: TranslationJob, batch_id: str, run_id: str) -> None:\n insert_batch_to_target(self.db, self.config_manager, job, batch_id, run_id)\n# #endregion BatchProcessingService\n# #endregion BatchProcessingService\n" + "body": "# #region BatchProcessingService [C:4] [TYPE Module] [SEMANTICS translate, batch, process, classify, cache]\n# @BRIEF Batch processing for translation: classify rows (same-language/cache/preview/LLM),\n# call LLM service, persist TranslationRecord/TranslationLanguage rows.\n# Local language detection (lingua) replaces LLM-based detection.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [DictionaryManager], [LLMTranslationService], [LanguageDetectService]\n# @RELATION DEPENDS_ON -> [estimate_token_budget], [ConfigManager]\n# @PRE DB session is available. Job configuration is valid.\n# @POST TranslationBatch, TranslationRecord, TranslationLanguage rows created and committed.\n# @SIDE_EFFECT LLM API calls via LLMTranslationService; DB writes.\n# @RATIONALE Extracted from TranslationExecutor. Batch insert delegated to _batch_insert.py.\n# @REJECTED Keeping batch processing inside TranslationExecutor — caused class to exceed INV_7.\n\nfrom datetime import UTC, datetime\nimport time\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationBatch, TranslationJob, TranslationLanguage, TranslationRecord\nfrom ...services.llm_provider import LLMProviderService\nfrom ._batch_insert import insert_batch_to_target\nfrom ._lang_detect import batch_detect\nfrom ._llm_call import LLMTranslationService\nfrom ._token_budget import estimate_token_budget\nfrom ._utils import _check_translation_cache, _compute_key_hash, _compute_source_hash\nfrom .dictionary import DictionaryManager\n\n\n# #region BatchProcessingService [C:4] [TYPE Class]\n# @BRIEF Create batch records, classify rows, process LLM calls, persist results.\nclass BatchProcessingService:\n \"\"\"Process a batch: classify (cache/preview/LLM), persist, and insert to target.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager) -> None:\n self.db = db\n self.config_manager = config_manager\n self._llm_service = LLMTranslationService(db)\n\n # #region process_batch [C:3] [TYPE Function]\n # @BRIEF Process a single batch: create record, classify rows, call LLM, persist.\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, job: TranslationJob, run_id: str, batch_index: int,\n batch_rows: list[dict[str, Any]],\n dict_snapshot_hash: str | None = None, config_hash: str | None = None,\n preview_edits_cache: dict[str, dict[str, str]] | None = None,\n ) -> dict[str, int]:\n \"\"\"Process a single batch: classify rows, call LLM (if needed), persist records.\"\"\"\n with belief_scope(\"BatchProcessingService.process_batch\"):\n batch_start = time.monotonic()\n batch = self._create_batch(run_id, batch_index, batch_rows)\n bid = batch.id\n result = {\"successful\": 0, \"failed\": 0, \"skipped\": 0, \"retries\": 0}\n\n tls = job.target_languages or [job.target_dialect or \"en\"]\n tls = [str(tls)] if not isinstance(tls, list) else tls\n\n # ★ Run local language detection on all rows (heuristic, no LLM)\n self._detect_languages(batch_rows, tls)\n\n source_texts = [r.get(\"source_text\", \"\") for r in batch_rows if r.get(\"source_text\")]\n rc = batch_rows[0].get(\"source_data\") if batch_rows else None\n dict_matches = DictionaryManager.filter_for_batch(self.db, source_texts, job.id, row_context=rc)\n\n self._check_cache(job, batch_rows, dict_snapshot_hash, config_hash)\n llm_rows, pre_rows = self._classify(batch_rows, preview_edits_cache, tls)\n\n result[\"successful\"] += self._persist_pre(pre_rows, bid, run_id, tls)\n if llm_rows:\n llm_res = self._process_llm(job, run_id, llm_rows, dict_matches, bid, tls)\n for k in (\"successful\", \"failed\", \"skipped\", \"retries\"):\n result[k] += llm_res.get(k, 0)\n\n batch.successful_records = result[\"successful\"]\n batch.failed_records = result[\"failed\"]\n batch.completed_at = datetime.now(UTC)\n batch.status = \"COMPLETED\" if result[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n latency = int((time.monotonic() - batch_start) * 1000)\n logger.reason(f\"Batch {batch_index} complete\", {\"batch_id\": bid, \"latency_ms\": latency, **result})\n return {**result, \"batch_id\": bid}\n # #endregion process_batch\n\n def _create_batch(self, run_id, batch_index, batch_rows):\n b = TranslationBatch(id=str(uuid.uuid4()), run_id=run_id, batch_index=batch_index,\n status=\"RUNNING\", total_records=len(batch_rows), started_at=datetime.now(UTC))\n self.db.add(b)\n self.db.flush()\n return b\n\n def _check_cache(self, job, batch_rows, dict_snapshot_hash, config_hash):\n for row in batch_rows:\n if row.get(\"approved_translation\"):\n continue\n st = row.get(\"source_text\", \"\")\n if not st:\n continue\n ctx = list(job.context_columns or [])\n h = _compute_source_hash(st, row.get(\"source_data\"), dict_snapshot_hash, config_hash, ctx)\n row[\"_source_hash\"] = h\n cached = _check_translation_cache(self.db, h)\n if cached:\n row[\"_cached_lang_values\"] = cached\n logger.reason(\"Translation cache hit\", {\"source_hash\": h[:12], \"langs\": list(cached.keys())})\n\n # ★ Local language detection — replaces LLM-based detection\n def _detect_languages(self, batch_rows: list[dict], target_languages: list[str]) -> None:\n \"\"\"Run local language detection on all batch rows (no LLM).\n\n Attaches '_detected_lang' (BCP-47 code or 'und') to each row dict.\n Uses batch_detect() for efficient multi-text processing.\n \"\"\"\n texts = [row.get(\"source_text\", \"\") for row in batch_rows]\n results = batch_detect(texts, target_languages)\n for row, lang in zip(batch_rows, results):\n row[\"_detected_lang\"] = lang\n\n def _classify(self, batch_rows, preview_edits_cache, tls):\n llm_rows, pre_rows = [], []\n tls_lower = [str(t).lower() for t in tls]\n for row in batch_rows:\n # ★ NEW: Same-language pre-filter — detected language matches target\n dl = row.get(\"_detected_lang\")\n if dl and str(dl).lower() not in (\"und\", \"\") and str(dl).lower() in tls_lower:\n row[\"_same_language\"] = True\n pre_rows.append(row)\n continue\n\n if row.get(\"approved_translation\"):\n pre_rows.append(row)\n continue\n cl = row.get(\"_cached_lang_values\")\n if cl:\n if all(lc in cl for lc in tls):\n pre_rows.append(row)\n continue\n if preview_edits_cache:\n sd = row.get(\"source_data\") or {}\n if sd:\n kh = _compute_key_hash(sd)\n pe = preview_edits_cache.get(kh)\n if pe:\n fe = next(iter(pe.values()), None)\n if fe:\n row[\"approved_translation\"] = fe\n pre_rows.append(row)\n continue\n llm_rows.append(row)\n return llm_rows, pre_rows\n\n def _persist_pre(self, pre_rows, bid, run_id, tls):\n count = 0\n for row in pre_rows:\n # ★ Same-language row: source language = target → no translation needed\n if row.get(\"_same_language\"):\n dl = str(row[\"_detected_lang\"])\n source_text = row.get(\"source_text\", \"\")\n rec = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=bid, run_id=run_id,\n source_sql=source_text, target_sql=source_text,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(rec)\n # One TranslationLanguage for the detected (same) language\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=rec.id, language_code=dl,\n source_language_detected=dl, translated_value=source_text,\n final_value=source_text, status=\"translated\", needs_review=False,\n ))\n count += 1\n continue\n\n # Existing: cached / approved rows\n cl = row.get(\"_cached_lang_values\")\n detected_lang = row.get(\"_detected_lang\", \"und\") or \"und\"\n rec = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=bid, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=row.get(\"approved_translation\", \"\"),\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(rec)\n for lc in tls:\n fv = cl[lc] if (cl and lc in cl) else row.get(\"approved_translation\", \"\")\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=rec.id, language_code=lc,\n source_language_detected=detected_lang, translated_value=fv,\n final_value=fv, status=\"translated\", needs_review=False,\n ))\n count += 1\n return count\n\n def _process_llm(self, job, run_id, rows_for_llm, dict_matches, bid, tls):\n provider_model = None\n if job.provider_id:\n try:\n p = LLMProviderService(self.db).get_provider(job.provider_id)\n if p:\n provider_model = p.default_model or \"gpt-4o-mini\"\n except Exception:\n provider_model = None\n\n tb = estimate_token_budget(\n source_rows=rows_for_llm, target_languages=tls,\n source_column=\"source_text\", context_columns=None,\n dictionary_entries=dict_matches, batch_size=len(rows_for_llm),\n provider_info=provider_model,\n )\n if tb[\"warning\"]:\n logger.explore(\"Token budget warning\", {\"batch_id\": bid, \"warning\": tb[\"warning\"]})\n\n return self._llm_service.call_llm_for_batch(\n job=job, run_id=run_id, batch_rows=rows_for_llm,\n dict_matches=dict_matches, batch_id=bid,\n max_tokens=tb[\"max_output_needed\"],\n )\n\n # -- Batch insert (delegation) --\n def insert_batch_to_target(self, job: TranslationJob, batch_id: str, run_id: str) -> None:\n insert_batch_to_target(self.db, self.config_manager, job, batch_id, run_id)\n# #endregion BatchProcessingService\n# #endregion BatchProcessingService\n" }, { "contract_id": "process_batch", @@ -36792,7 +37653,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/_batch_sizer.py", "start_line": 1, - "end_line": 223, + "end_line": 221, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36803,14 +37664,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AdaptiveBatchSizer [C:3] [TYPE Module] [SEMANTICS translate, batch, sizing, token-budget]\n# @BRIEF Adaptive batch sizing for LLM translation — splits source rows into variable-sized\n# batches based on actual content length and token budget estimates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [estimate_token_budget]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RATIONALE Extracted from TranslationExecutor to comply with INV_7 (module < 400 lines).\n# Fixed batch_size of 50 wastes LLM context for short rows and overflows for\n# long rows. Variable sizing maximizes throughput while preventing truncation.\n# @REJECTED Fixed batch_size of 50 — causes truncation on long-content rows.\n# Single monolithic batch — would lose all progress on any failure.\n\nimport time\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob\nfrom ...services.llm_provider import LLMProviderService\nfrom ._token_budget import (\n JSON_OVERHEAD_PER_ROW,\n MAX_OUTPUT_HEADROOM,\n OUTPUT_PER_ROW_PER_LANG,\n PROMPT_BASE_TOKENS,\n REASONING_OVERHEAD,\n estimate_token_budget,\n)\nfrom ._utils import estimate_row_tokens\n\n\n# #region AdaptiveBatchSizer [C:3] [TYPE Class]\n# @BRIEF Split source rows into auto-sized batches based on token budget estimates.\nclass AdaptiveBatchSizer:\n \"\"\"Split source rows into auto-sized batches based on token budget estimates.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager) -> None:\n self.db = db\n self.config_manager = config_manager\n\n # #region resolve_provider_model [C:2] [TYPE Function] [SEMANTICS llm, provider, model]\n # @BRIEF Resolve the LLM provider model name for token budget estimation.\n # @POST Returns model name string or None if resolution fails.\n # @SIDE_EFFECT DB query to LLM provider table.\n def resolve_provider_model(self, job: TranslationJob) -> str | None:\n \"\"\"Resolve the provider model name for token budget estimation.\"\"\"\n if not job.provider_id:\n return None\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.get_provider(job.provider_id)\n if p:\n return p.default_model or \"gpt-4o-mini\"\n except Exception:\n pass\n return None\n # #endregion resolve_provider_model\n\n # #region auto_size_batches [C:3] [TYPE Function] [SEMANTICS translate, batch, sizing]\n # @BRIEF Split source rows into variable-sized batches based on content length.\n # @PRE source_rows is non-empty. job has valid config.\n # @POST Returns list of batches, each batch is a list of row dicts.\n # Each batch fits within the estimated token budget for its rows.\n # @SIDE_EFFECT DB query to resolve provider model. Logs batch statistics.\n def auto_size_batches(\n self,\n job: TranslationJob,\n source_rows: list[dict],\n target_languages: list[str],\n provider_info: str | None = None,\n ) -> list[list[dict]]:\n \"\"\"Split source rows into auto-sized batches based on content length.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n with belief_scope(\"AdaptiveBatchSizer.auto_size_batches\"):\n if not source_rows:\n return []\n\n if provider_info is None:\n provider_info = self.resolve_provider_model(job)\n\n # 1. Estimate per-row token counts\n row_tokens: list[int] = []\n for row in source_rows:\n source_text = row.get(\"source_text\", \"\")\n source_data = row.get(\"source_data\")\n tokens = estimate_row_tokens(source_text, source_data, job)\n row_tokens.append(tokens)\n\n # 2. Get budget recommendation\n budget = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column or \"source_text\",\n context_columns=job.context_columns,\n batch_size=len(source_rows),\n provider_info=provider_info,\n )\n\n recommended = budget.get(\"batch_size_adjusted\", 0)\n\n # Fallback: if budget calculation fails, use fixed size\n if recommended <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Token budget returned zero — falling back to fixed batch size\", {\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 3. Compute per-batch row-content budget\n # Use the ACTUAL available input capacity (context_window - max_output_tokens),\n # NOT the sum of the first N rows. This prevents long rows from being\n # incorrectly placed in 1-row batches when the context window has plenty of room.\n available_input = budget.get(\"available_input_budget\")\n if available_input is not None:\n # New-style: use actual available input capacity\n per_batch_budget = available_input - PROMPT_BASE_TOKENS\n else:\n # Fallback for tests: use estimated_input (sum of first N rows)\n estimated_input = budget.get(\"estimated_input_tokens\", 50000)\n per_batch_budget = estimated_input - PROMPT_BASE_TOKENS\n\n if per_batch_budget <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Per-batch budget collapsed — falling back to fixed batch size\", {\n \"per_batch_budget\": per_batch_budget,\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 4. Greedy batch splitting\n # Compute max rows per batch from OUTPUT constraint.\n # This prevents output truncation (finish_reason=length) when batching\n # many short rows within the large input budget.\n num_languages = len(target_languages)\n max_output_tokens_val = budget.get(\"max_output_tokens\")\n if max_output_tokens_val is not None:\n output_per_row = num_languages * OUTPUT_PER_ROW_PER_LANG + JSON_OVERHEAD_PER_ROW\n available_output = max_output_tokens_val - REASONING_OVERHEAD - MAX_OUTPUT_HEADROOM\n max_rows_by_output = max(available_output // output_per_row, 1) if output_per_row > 0 else 20\n max_rows_hard_cap = max_rows_by_output\n else:\n # Fallback for tests: old formula\n max_rows_hard_cap = max(recommended * 2, 20)\n\n # 5. Respect job.batch_size as the absolute maximum rows per batch.\n # User-configured batch_size overrides model-based estimates to\n # prevent LLM quality degradation on large batches.\n if job.batch_size:\n max_rows_hard_cap = min(max_rows_hard_cap, job.batch_size)\n\n batches: list[list[dict]] = []\n current_batch: list[dict] = []\n current_tokens = 0\n\n for i, row in enumerate(source_rows):\n rt = row_tokens[i]\n\n if rt > per_batch_budget:\n if current_batch:\n batches.append(current_batch)\n current_batch = []\n current_tokens = 0\n logger.reason(\"Single row exceeds per-batch token budget — placing in own batch\", {\n \"row_index\": i,\n \"row_tokens\": rt,\n \"per_batch_budget\": per_batch_budget,\n })\n batches.append([row])\n continue\n\n should_split = bool(\n current_batch\n and (current_tokens + rt > per_batch_budget\n or len(current_batch) >= max_rows_hard_cap)\n )\n\n if should_split:\n batches.append(current_batch)\n current_batch = [row]\n current_tokens = rt\n else:\n current_batch.append(row)\n current_tokens += rt\n\n if current_batch:\n batches.append(current_batch)\n\n # 5. Log adaptive batch statistics\n if batches:\n avg_size = sum(len(b) for b in batches) / max(1, len(batches))\n max_size = max(len(b) for b in batches)\n logger.reason(\"Auto-sized batches\", {\n \"total_rows\": len(source_rows),\n \"num_batches\": len(batches),\n \"avg_batch_size\": round(avg_size, 1),\n \"max_batch_size\": max_size,\n \"recommended_batch_size\": recommended,\n \"per_batch_budget\": per_batch_budget,\n })\n\n return batches\n # #endregion auto_size_batches\n# #endregion AdaptiveBatchSizer\n# #endregion AdaptiveBatchSizer\n" + "body": "# #region AdaptiveBatchSizer [C:3] [TYPE Module] [SEMANTICS translate, batch, sizing, token-budget]\n# @BRIEF Adaptive batch sizing for LLM translation — splits source rows into variable-sized\n# batches based on actual content length and token budget estimates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [estimate_token_budget]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RATIONALE Extracted from TranslationExecutor to comply with INV_7 (module < 400 lines).\n# Fixed batch_size of 50 wastes LLM context for short rows and overflows for\n# long rows. Variable sizing maximizes throughput while preventing truncation.\n# @REJECTED Fixed batch_size of 50 — causes truncation on long-content rows.\n# Single monolithic batch — would lose all progress on any failure.\n\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob\nfrom ...services.llm_provider import LLMProviderService\nfrom ._token_budget import (\n JSON_OVERHEAD_PER_ROW,\n MAX_OUTPUT_HEADROOM,\n OUTPUT_PER_ROW_PER_LANG,\n PROMPT_BASE_TOKENS,\n REASONING_OVERHEAD,\n estimate_token_budget,\n)\nfrom ._utils import estimate_row_tokens\n\n\n# #region AdaptiveBatchSizer [C:3] [TYPE Class]\n# @BRIEF Split source rows into auto-sized batches based on token budget estimates.\nclass AdaptiveBatchSizer:\n \"\"\"Split source rows into auto-sized batches based on token budget estimates.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager) -> None:\n self.db = db\n self.config_manager = config_manager\n\n # #region resolve_provider_model [C:2] [TYPE Function] [SEMANTICS llm, provider, model]\n # @BRIEF Resolve the LLM provider model name for token budget estimation.\n # @POST Returns model name string or None if resolution fails.\n # @SIDE_EFFECT DB query to LLM provider table.\n def resolve_provider_model(self, job: TranslationJob) -> str | None:\n \"\"\"Resolve the provider model name for token budget estimation.\"\"\"\n if not job.provider_id:\n return None\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.get_provider(job.provider_id)\n if p:\n return p.default_model or \"gpt-4o-mini\"\n except Exception:\n pass\n return None\n # #endregion resolve_provider_model\n\n # #region auto_size_batches [C:3] [TYPE Function] [SEMANTICS translate, batch, sizing]\n # @BRIEF Split source rows into variable-sized batches based on content length.\n # @PRE source_rows is non-empty. job has valid config.\n # @POST Returns list of batches, each batch is a list of row dicts.\n # Each batch fits within the estimated token budget for its rows.\n # @SIDE_EFFECT DB query to resolve provider model. Logs batch statistics.\n def auto_size_batches(\n self,\n job: TranslationJob,\n source_rows: list[dict],\n target_languages: list[str],\n provider_info: str | None = None,\n ) -> list[list[dict]]:\n \"\"\"Split source rows into auto-sized batches based on content length.\n\n Each batch is sized so that its total estimated tokens fit within the\n available context window (input budget), accounting for prompt overhead,\n dictionary entries, and output tokens.\n \"\"\"\n with belief_scope(\"AdaptiveBatchSizer.auto_size_batches\"):\n if not source_rows:\n return []\n\n if provider_info is None:\n provider_info = self.resolve_provider_model(job)\n\n # 1. Estimate per-row token counts\n row_tokens: list[int] = []\n for row in source_rows:\n source_text = row.get(\"source_text\", \"\")\n source_data = row.get(\"source_data\")\n tokens = estimate_row_tokens(source_text, source_data, job)\n row_tokens.append(tokens)\n\n # 2. Get budget recommendation\n budget = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column or \"source_text\",\n context_columns=job.context_columns,\n batch_size=len(source_rows),\n provider_info=provider_info,\n )\n\n recommended = budget.get(\"batch_size_adjusted\", 0)\n\n # Fallback: if budget calculation fails, use fixed size\n if recommended <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Token budget returned zero — falling back to fixed batch size\", {\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 3. Compute per-batch row-content budget\n # Use the ACTUAL available input capacity (context_window - max_output_tokens),\n # NOT the sum of the first N rows. This prevents long rows from being\n # incorrectly placed in 1-row batches when the context window has plenty of room.\n available_input = budget.get(\"available_input_budget\")\n if available_input is not None:\n # New-style: use actual available input capacity\n per_batch_budget = available_input - PROMPT_BASE_TOKENS\n else:\n # Fallback for tests: use estimated_input (sum of first N rows)\n estimated_input = budget.get(\"estimated_input_tokens\", 50000)\n per_batch_budget = estimated_input - PROMPT_BASE_TOKENS\n\n if per_batch_budget <= 0:\n fallback_size = job.batch_size or 50\n logger.explore(\"Per-batch budget collapsed — falling back to fixed batch size\", {\n \"per_batch_budget\": per_batch_budget,\n \"fallback_size\": fallback_size,\n \"total_rows\": len(source_rows),\n })\n return [\n source_rows[i:i + fallback_size]\n for i in range(0, len(source_rows), fallback_size)\n ]\n\n # 4. Greedy batch splitting\n # Compute max rows per batch from OUTPUT constraint.\n # This prevents output truncation (finish_reason=length) when batching\n # many short rows within the large input budget.\n num_languages = len(target_languages)\n max_output_tokens_val = budget.get(\"max_output_tokens\")\n if max_output_tokens_val is not None:\n output_per_row = num_languages * OUTPUT_PER_ROW_PER_LANG + JSON_OVERHEAD_PER_ROW\n available_output = max_output_tokens_val - REASONING_OVERHEAD - MAX_OUTPUT_HEADROOM\n max_rows_by_output = max(available_output // output_per_row, 1) if output_per_row > 0 else 20\n max_rows_hard_cap = max_rows_by_output\n else:\n # Fallback for tests: old formula\n max_rows_hard_cap = max(recommended * 2, 20)\n\n # 5. Respect job.batch_size as the absolute maximum rows per batch.\n # User-configured batch_size overrides model-based estimates to\n # prevent LLM quality degradation on large batches.\n if job.batch_size:\n max_rows_hard_cap = min(max_rows_hard_cap, job.batch_size)\n\n batches: list[list[dict]] = []\n current_batch: list[dict] = []\n current_tokens = 0\n\n for i, row in enumerate(source_rows):\n rt = row_tokens[i]\n\n if rt > per_batch_budget:\n if current_batch:\n batches.append(current_batch)\n current_batch = []\n current_tokens = 0\n logger.reason(\"Single row exceeds per-batch token budget — placing in own batch\", {\n \"row_index\": i,\n \"row_tokens\": rt,\n \"per_batch_budget\": per_batch_budget,\n })\n batches.append([row])\n continue\n\n should_split = bool(\n current_batch\n and (current_tokens + rt > per_batch_budget\n or len(current_batch) >= max_rows_hard_cap)\n )\n\n if should_split:\n batches.append(current_batch)\n current_batch = [row]\n current_tokens = rt\n else:\n current_batch.append(row)\n current_tokens += rt\n\n if current_batch:\n batches.append(current_batch)\n\n # 5. Log adaptive batch statistics\n if batches:\n avg_size = sum(len(b) for b in batches) / max(1, len(batches))\n max_size = max(len(b) for b in batches)\n logger.reason(\"Auto-sized batches\", {\n \"total_rows\": len(source_rows),\n \"num_batches\": len(batches),\n \"avg_batch_size\": round(avg_size, 1),\n \"max_batch_size\": max_size,\n \"recommended_batch_size\": recommended,\n \"per_batch_budget\": per_batch_budget,\n })\n\n return batches\n # #endregion auto_size_batches\n# #endregion AdaptiveBatchSizer\n# #endregion AdaptiveBatchSizer\n" }, { "contract_id": "resolve_provider_model", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_sizer.py", - "start_line": 48, - "end_line": 64, + "start_line": 46, + "end_line": 62, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -36829,8 +37690,8 @@ "contract_id": "auto_size_batches", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_batch_sizer.py", - "start_line": 66, - "end_line": 221, + "start_line": 64, + "end_line": 219, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -36996,7 +37857,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/_llm_call.py", "start_line": 1, - "end_line": 313, + "end_line": 312, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -37007,14 +37868,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMTranslationService [C:4] [TYPE Module] [SEMANTICS translate, llm, call, orchestrate, retry]\n# @BRIEF LLM interaction for batch translation: call provider with retry, handle truncation\n# by recursive splitting, enforce dictionary post-processing. Orchestrates HTTP calls\n# (_llm_http) and response parsing (_llm_parse).\n# Language detection is now handled locally (lingua) — LLM prompt no longer\n# requests detected_source_language; local _detected_lang takes priority.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [ContextAwarePromptBuilder]\n# @RELATION DEPENDS_ON -> [EXT:method:_llm_http], [_llm_parse]\n# @PRE DB session is available. LLM provider is configured on the job.\n# @POST TranslationRecord rows created for LLM-processed rows (success/fail/skip).\n# @SIDE_EFFECT HTTP calls to LLM provider API; DB writes.\n# @RATIONALE Core orchestration logic — HTTP and parse logic extracted to _llm_http.py and\n# _llm_parse.py to meet INV_7 module limit (< 400 lines).\n# @REJECTED Single monolithic call_llm_for_batch at 327 lines — split into focused sub-methods.\n\nfrom datetime import UTC, datetime\nimport json\nimport time\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationBatch, TranslationJob, TranslationLanguage, TranslationRecord\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...services.llm_provider import LLMProviderService\nfrom ._llm_http import call_openai_compatible\nfrom ._llm_parse import parse_llm_response\nfrom ._token_budget import estimate_token_budget\nfrom ._utils import _enforce_dictionary\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\nfrom .prompt_builder import ContextAwarePromptBuilder\n\nMAX_RETRIES_PER_BATCH = 3\n\n\n# #region LLMTranslationService [C:4] [TYPE Class]\n# @BRIEF Call LLM, handle retry/truncation, parse response, persist records.\nclass LLMTranslationService:\n \"\"\"LLM interaction for batch translation with retry, truncation handling, and parsing.\"\"\"\n\n def __init__(self, db: Session) -> None:\n self.db = db\n\n # #region call_llm_for_batch [C:3] [TYPE Function] [SEMANTICS translate, llm, batch, orchestrate]\n # @BRIEF Call LLM for a batch of rows requiring translation. Parse and persist results.\n # @PRE job has valid provider_id. batch_rows is non-empty.\n # @POST Returns dict with successful/failed/skipped counts.\n # @SIDE_EFFECT HTTP call to LLM provider; DB writes.\n def call_llm_for_batch(\n self, job: TranslationJob, run_id: str,\n batch_rows: list[dict[str, Any]], dict_matches: list[dict[str, Any]],\n batch_id: str, max_tokens: int = 8192, _recursion_depth: int = 0,\n ) -> dict[str, int]:\n \"\"\"Call LLM for a batch of rows; parse response; create records.\"\"\"\n with belief_scope(\"LLMTranslationService.call_llm_for_batch\"):\n dictionary_section = self._build_dictionary_section(dict_matches, batch_rows)\n target_languages = self._resolve_target_languages(job)\n prompt = self._build_prompt(job, batch_rows, dictionary_section, target_languages)\n\n llm_response, finish_reason, retries, last_error = self._call_llm_with_retry(\n job, prompt, batch_id, max_tokens,\n )\n if llm_response is None:\n return self._handle_llm_failure(batch_rows, run_id, batch_id, retries, last_error)\n\n if finish_reason == \"length\" and len(batch_rows) >= 2 and run_id:\n if _recursion_depth < MAX_RETRIES_PER_BATCH:\n return self._split_and_retry(job, run_id, batch_rows, dict_matches,\n batch_id, max_tokens, _recursion_depth, retries)\n logger.explore(\"Truncation recursion depth exceeded\", {\"batch_id\": batch_id, \"depth\": _recursion_depth})\n\n try:\n translations = parse_llm_response(llm_response, len(batch_rows), target_languages=target_languages)\n except ValueError as e:\n return self._handle_parse_failure(batch_rows, run_id, batch_id, retries, e)\n\n return self._create_records_from_translations(\n batch_rows, run_id, batch_id, target_languages, translations, dict_matches, retries,\n )\n # #endregion call_llm_for_batch\n\n def _build_dictionary_section(self, dict_matches, batch_rows) -> str:\n if not dict_matches:\n return \"\"\n row_context = batch_rows[0].get(\"source_data\") if batch_rows else None\n annotated = ContextAwarePromptBuilder.build_context_entries(dict_matches, row_context)\n return \"Terminology dictionary (use these translations when applicable):\\n\" + \\\n \"\\n\".join(f\"- {a}\" for a in annotated) + \"\\n\\n\"\n\n @staticmethod\n def _resolve_target_languages(job):\n langs = job.target_languages or [job.target_dialect or \"en\"]\n return [str(langs)] if not isinstance(langs, list) else langs\n\n @staticmethod\n def _build_prompt(job, batch_rows, dictionary_section, target_languages):\n target_languages_str = \", \".join(target_languages)\n rows_json = json.dumps([\n {\"row_id\": str(row.get(\"row_index\", idx)), \"text\": row.get(\"source_text\", \"\")}\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n return render_prompt(DEFAULT_EXECUTION_PROMPT_TEMPLATE, {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": target_languages_str,\n \"target_languages\": target_languages_str,\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 def _call_llm_with_retry(self, job, prompt, batch_id, max_tokens):\n llm_response = None\n last_error = None\n retries = 0\n finish_reason = None\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response, finish_reason = self.call_llm(job, prompt, max_tokens=max_tokens)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n logger.explore(f\"LLM call failed (attempt {attempt})\", {\"batch_id\": batch_id, \"error\": last_error})\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt)\n return llm_response, finish_reason, retries, last_error\n\n def _handle_llm_failure(self, batch_rows, run_id, batch_id, retries, last_error):\n for row in batch_rows:\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=None,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n ))\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n def _split_and_retry(self, job, run_id, batch_rows, dict_matches, batch_id, max_tokens, depth, retries):\n mid = len(batch_rows) // 2\n logger.explore(\"LLM output truncated — splitting batch\",\n {\"batch_id\": batch_id, \"batch_size\": len(batch_rows), \"split_at\": mid, \"depth\": depth})\n\n # Create proper TranslationBatch rows for child halves (fixes FK violation\n # where _L/_R string suffixes referenced non-existent translation_batches rows).\n # @RATIONALE Previous code appended \"_L\"/\"_R\" to batch_id string, violating\n # FK translation_records.batch_id → translation_batches.id.\n # @REJECTED Continuing with string-suffixed batch_ids — causes FK violation\n # when any child record is flushed.\n left_batch = TranslationBatch(\n id=str(uuid.uuid4()), run_id=run_id, batch_index=-1,\n status=\"RUNNING\", total_records=len(batch_rows[:mid]),\n started_at=datetime.now(UTC),\n )\n right_batch = TranslationBatch(\n id=str(uuid.uuid4()), run_id=run_id, batch_index=-1,\n status=\"RUNNING\", total_records=len(batch_rows[mid:]),\n started_at=datetime.now(UTC),\n )\n self.db.add_all([left_batch, right_batch])\n self.db.flush()\n\n left = self.call_llm_for_batch(job, run_id, batch_rows[:mid], dict_matches,\n left_batch.id, max_tokens, depth + 1)\n right = self.call_llm_for_batch(job, run_id, batch_rows[mid:], dict_matches,\n right_batch.id, max_tokens, depth + 1)\n\n # Finalise child batch stats\n left_batch.completed_at = datetime.now(UTC)\n right_batch.completed_at = datetime.now(UTC)\n left_batch.successful_records = left[\"successful\"]\n right_batch.successful_records = right[\"successful\"]\n left_batch.failed_records = left[\"failed\"]\n right_batch.failed_records = right[\"failed\"]\n left_batch.status = \"COMPLETED\" if left[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n right_batch.status = \"COMPLETED\" if right[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n return {\"successful\": left[\"successful\"] + right[\"successful\"],\n \"failed\": left[\"failed\"] + right[\"failed\"],\n \"skipped\": left[\"skipped\"] + right[\"skipped\"],\n \"retries\": retries + left.get(\"retries\", 0) + right.get(\"retries\", 0)}\n\n def _handle_parse_failure(self, batch_rows, run_id, batch_id, retries, error):\n for row in batch_rows:\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=None,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {error}\",\n ))\n return {\"successful\": 0, \"failed\": 0, \"skipped\": len(batch_rows), \"retries\": retries}\n\n def _create_records_from_translations(self, batch_rows, run_id, batch_id, target_languages, translations, dict_matches, retries):\n successful = failed = skipped = 0\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n td = translations.get(row_id)\n source_text = row.get(\"source_text\", \"\")\n # ★ Local detection (from _batch_proc._detect_languages) takes priority.\n # Fallback to LLM response field for backward compatibility.\n detected_lang = row.get(\"_detected_lang\", \"und\") or \"und\"\n if detected_lang == \"und\" and td:\n detected_lang = td.get(\"detected_source_language\", \"und\") or \"und\"\n if td is None:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"NULL translation\")\n continue\n plv = self._extract_per_lang_values(td, target_languages)\n if dict_matches and source_text:\n _enforce_dictionary(source_text, plv, dict_matches, batch_id, row_id)\n if not plv:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"Empty translation\")\n continue\n successful += 1\n primary = next(iter(plv.values()), \"\")\n record = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=primary,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n for lang_code in target_languages:\n if detected_lang != \"und\" and str(lang_code).lower() == str(detected_lang).lower():\n continue\n val = plv.get(lang_code, \"\")\n needs_review = (detected_lang == \"und\")\n if needs_review:\n logger.explore(\"undetected language\", {\"record_id\": row_id, \"language_code\": lang_code, \"text\": source_text[:100]})\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=record.id, language_code=lang_code,\n source_language_detected=detected_lang, translated_value=val or \"\",\n final_value=val or \"\", status=\"translated\", needs_review=needs_review,\n ))\n return {\"successful\": successful, \"failed\": failed, \"skipped\": skipped, \"retries\": retries}\n\n @staticmethod\n def _extract_per_lang_values(td, target_languages):\n plv = {}\n has_any = False\n for lc in target_languages:\n lv = td.get(lc)\n if lv is not None and str(lv).strip():\n plv[lc] = str(lv)\n has_any = True\n if not has_any:\n t = td.get(\"translation\", \"\")\n if t.strip():\n plv[target_languages[0]] = t\n has_any = True\n return plv if has_any else {}\n\n def _add_skipped(self, row, run_id, batch_id, source_text, reason):\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=\"\",\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"SKIPPED\",\n error_message=reason,\n ))\n\n # #region call_llm [C:3] [TYPE Function] [SEMANTICS translate, llm, call]\n # @BRIEF Route to provider-specific LLM call implementation.\n def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> tuple[str, str | None]:\n \"\"\"Call the configured LLM provider with the batch prompt.\"\"\"\n with belief_scope(\"LLMTranslationService.call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\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 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 model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type not in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n return call_openai_compatible(\n base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt,\n provider_type=provider_type, max_tokens=max_tokens, disable_reasoning=disable_reasoning,\n )\n # #endregion call_llm\n\n # -- Static methods delegated to sub-modules for backward compat --\n @staticmethod\n def call_openai_compatible(*a, **kw):\n return call_openai_compatible(*a, **kw)\n\n @staticmethod\n def _parse_llm_response(*a, **kw):\n return parse_llm_response(*a, **kw)\n# #endregion LLMTranslationService\n# #endregion LLMTranslationService\n" + "body": "# #region LLMTranslationService [C:4] [TYPE Module] [SEMANTICS translate, llm, call, orchestrate, retry]\n# @BRIEF LLM interaction for batch translation: call provider with retry, handle truncation\n# by recursive splitting, enforce dictionary post-processing. Orchestrates HTTP calls\n# (_llm_http) and response parsing (_llm_parse).\n# Language detection is now handled locally (lingua) — LLM prompt no longer\n# requests detected_source_language; local _detected_lang takes priority.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [TranslationRecord], [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [ContextAwarePromptBuilder]\n# @RELATION DEPENDS_ON -> [EXT:method:_llm_http], [_llm_parse]\n# @PRE DB session is available. LLM provider is configured on the job.\n# @POST TranslationRecord rows created for LLM-processed rows (success/fail/skip).\n# @SIDE_EFFECT HTTP calls to LLM provider API; DB writes.\n# @RATIONALE Core orchestration logic — HTTP and parse logic extracted to _llm_http.py and\n# _llm_parse.py to meet INV_7 module limit (< 400 lines).\n# @REJECTED Single monolithic call_llm_for_batch at 327 lines — split into focused sub-methods.\n\nfrom datetime import UTC, datetime\nimport json\nimport time\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationBatch, TranslationJob, TranslationLanguage, TranslationRecord\nfrom ...services.llm_prompt_templates import render_prompt\nfrom ...services.llm_provider import LLMProviderService\nfrom ._llm_http import call_openai_compatible\nfrom ._llm_parse import parse_llm_response\nfrom ._utils import _enforce_dictionary\nfrom .preview import DEFAULT_EXECUTION_PROMPT_TEMPLATE\nfrom .prompt_builder import ContextAwarePromptBuilder\n\nMAX_RETRIES_PER_BATCH = 3\n\n\n# #region LLMTranslationService [C:4] [TYPE Class]\n# @BRIEF Call LLM, handle retry/truncation, parse response, persist records.\nclass LLMTranslationService:\n \"\"\"LLM interaction for batch translation with retry, truncation handling, and parsing.\"\"\"\n\n def __init__(self, db: Session) -> None:\n self.db = db\n\n # #region call_llm_for_batch [C:3] [TYPE Function] [SEMANTICS translate, llm, batch, orchestrate]\n # @BRIEF Call LLM for a batch of rows requiring translation. Parse and persist results.\n # @PRE job has valid provider_id. batch_rows is non-empty.\n # @POST Returns dict with successful/failed/skipped counts.\n # @SIDE_EFFECT HTTP call to LLM provider; DB writes.\n def call_llm_for_batch(\n self, job: TranslationJob, run_id: str,\n batch_rows: list[dict[str, Any]], dict_matches: list[dict[str, Any]],\n batch_id: str, max_tokens: int = 8192, _recursion_depth: int = 0,\n ) -> dict[str, int]:\n \"\"\"Call LLM for a batch of rows; parse response; create records.\"\"\"\n with belief_scope(\"LLMTranslationService.call_llm_for_batch\"):\n dictionary_section = self._build_dictionary_section(dict_matches, batch_rows)\n target_languages = self._resolve_target_languages(job)\n prompt = self._build_prompt(job, batch_rows, dictionary_section, target_languages)\n\n llm_response, finish_reason, retries, last_error = self._call_llm_with_retry(\n job, prompt, batch_id, max_tokens,\n )\n if llm_response is None:\n return self._handle_llm_failure(batch_rows, run_id, batch_id, retries, last_error)\n\n if finish_reason == \"length\" and len(batch_rows) >= 2 and run_id:\n if _recursion_depth < MAX_RETRIES_PER_BATCH:\n return self._split_and_retry(job, run_id, batch_rows, dict_matches,\n batch_id, max_tokens, _recursion_depth, retries)\n logger.explore(\"Truncation recursion depth exceeded\", {\"batch_id\": batch_id, \"depth\": _recursion_depth})\n\n try:\n translations = parse_llm_response(llm_response, len(batch_rows), target_languages=target_languages)\n except ValueError as e:\n return self._handle_parse_failure(batch_rows, run_id, batch_id, retries, e)\n\n return self._create_records_from_translations(\n batch_rows, run_id, batch_id, target_languages, translations, dict_matches, retries,\n )\n # #endregion call_llm_for_batch\n\n def _build_dictionary_section(self, dict_matches, batch_rows) -> str:\n if not dict_matches:\n return \"\"\n row_context = batch_rows[0].get(\"source_data\") if batch_rows else None\n annotated = ContextAwarePromptBuilder.build_context_entries(dict_matches, row_context)\n return \"Terminology dictionary (use these translations when applicable):\\n\" + \\\n \"\\n\".join(f\"- {a}\" for a in annotated) + \"\\n\\n\"\n\n @staticmethod\n def _resolve_target_languages(job):\n langs = job.target_languages or [job.target_dialect or \"en\"]\n return [str(langs)] if not isinstance(langs, list) else langs\n\n @staticmethod\n def _build_prompt(job, batch_rows, dictionary_section, target_languages):\n target_languages_str = \", \".join(target_languages)\n rows_json = json.dumps([\n {\"row_id\": str(row.get(\"row_index\", idx)), \"text\": row.get(\"source_text\", \"\")}\n for idx, row in enumerate(batch_rows)\n ], indent=2)\n return render_prompt(DEFAULT_EXECUTION_PROMPT_TEMPLATE, {\n \"source_language\": job.source_dialect or \"SQL\",\n \"target_language\": target_languages_str,\n \"target_languages\": target_languages_str,\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 def _call_llm_with_retry(self, job, prompt, batch_id, max_tokens):\n llm_response = None\n last_error = None\n retries = 0\n finish_reason = None\n for attempt in range(1, MAX_RETRIES_PER_BATCH + 1):\n try:\n llm_response, finish_reason = self.call_llm(job, prompt, max_tokens=max_tokens)\n break\n except Exception as e:\n last_error = str(e)\n retries += 1\n logger.explore(f\"LLM call failed (attempt {attempt})\", {\"batch_id\": batch_id, \"error\": last_error})\n if attempt < MAX_RETRIES_PER_BATCH:\n time.sleep(2 ** attempt)\n return llm_response, finish_reason, retries, last_error\n\n def _handle_llm_failure(self, batch_rows, run_id, batch_id, retries, last_error):\n for row in batch_rows:\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=None,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"FAILED\",\n error_message=f\"LLM call failed after {retries} retries: {last_error}\",\n ))\n return {\"successful\": 0, \"failed\": len(batch_rows), \"skipped\": 0, \"retries\": retries}\n\n def _split_and_retry(self, job, run_id, batch_rows, dict_matches, batch_id, max_tokens, depth, retries):\n mid = len(batch_rows) // 2\n logger.explore(\"LLM output truncated — splitting batch\",\n {\"batch_id\": batch_id, \"batch_size\": len(batch_rows), \"split_at\": mid, \"depth\": depth})\n\n # Create proper TranslationBatch rows for child halves (fixes FK violation\n # where _L/_R string suffixes referenced non-existent translation_batches rows).\n # @RATIONALE Previous code appended \"_L\"/\"_R\" to batch_id string, violating\n # FK translation_records.batch_id → translation_batches.id.\n # @REJECTED Continuing with string-suffixed batch_ids — causes FK violation\n # when any child record is flushed.\n left_batch = TranslationBatch(\n id=str(uuid.uuid4()), run_id=run_id, batch_index=-1,\n status=\"RUNNING\", total_records=len(batch_rows[:mid]),\n started_at=datetime.now(UTC),\n )\n right_batch = TranslationBatch(\n id=str(uuid.uuid4()), run_id=run_id, batch_index=-1,\n status=\"RUNNING\", total_records=len(batch_rows[mid:]),\n started_at=datetime.now(UTC),\n )\n self.db.add_all([left_batch, right_batch])\n self.db.flush()\n\n left = self.call_llm_for_batch(job, run_id, batch_rows[:mid], dict_matches,\n left_batch.id, max_tokens, depth + 1)\n right = self.call_llm_for_batch(job, run_id, batch_rows[mid:], dict_matches,\n right_batch.id, max_tokens, depth + 1)\n\n # Finalise child batch stats\n left_batch.completed_at = datetime.now(UTC)\n right_batch.completed_at = datetime.now(UTC)\n left_batch.successful_records = left[\"successful\"]\n right_batch.successful_records = right[\"successful\"]\n left_batch.failed_records = left[\"failed\"]\n right_batch.failed_records = right[\"failed\"]\n left_batch.status = \"COMPLETED\" if left[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n right_batch.status = \"COMPLETED\" if right[\"failed\"] == 0 else \"COMPLETED_WITH_ERRORS\"\n self.db.flush()\n\n return {\"successful\": left[\"successful\"] + right[\"successful\"],\n \"failed\": left[\"failed\"] + right[\"failed\"],\n \"skipped\": left[\"skipped\"] + right[\"skipped\"],\n \"retries\": retries + left.get(\"retries\", 0) + right.get(\"retries\", 0)}\n\n def _handle_parse_failure(self, batch_rows, run_id, batch_id, retries, error):\n for row in batch_rows:\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=row.get(\"source_text\", \"\"), target_sql=None,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"SKIPPED\",\n error_message=f\"LLM parse failure: {error}\",\n ))\n return {\"successful\": 0, \"failed\": 0, \"skipped\": len(batch_rows), \"retries\": retries}\n\n def _create_records_from_translations(self, batch_rows, run_id, batch_id, target_languages, translations, dict_matches, retries):\n successful = failed = skipped = 0\n for row in batch_rows:\n row_id = str(row.get(\"row_index\", \"\"))\n td = translations.get(row_id)\n source_text = row.get(\"source_text\", \"\")\n # ★ Local detection (from _batch_proc._detect_languages) takes priority.\n # Fallback to LLM response field for backward compatibility.\n detected_lang = row.get(\"_detected_lang\", \"und\") or \"und\"\n if detected_lang == \"und\" and td:\n detected_lang = td.get(\"detected_source_language\", \"und\") or \"und\"\n if td is None:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"NULL translation\")\n continue\n plv = self._extract_per_lang_values(td, target_languages)\n if dict_matches and source_text:\n _enforce_dictionary(source_text, plv, dict_matches, batch_id, row_id)\n if not plv:\n skipped += 1\n self._add_skipped(row, run_id, batch_id, source_text, \"Empty translation\")\n continue\n successful += 1\n primary = next(iter(plv.values()), \"\")\n record = TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=primary,\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), source_hash=row.get(\"_source_hash\"),\n status=\"SUCCESS\",\n )\n self.db.add(record)\n for lang_code in target_languages:\n if detected_lang != \"und\" and str(lang_code).lower() == str(detected_lang).lower():\n continue\n val = plv.get(lang_code, \"\")\n needs_review = (detected_lang == \"und\")\n if needs_review:\n logger.explore(\"undetected language\", {\"record_id\": row_id, \"language_code\": lang_code, \"text\": source_text[:100]})\n self.db.add(TranslationLanguage(\n id=str(uuid.uuid4()), record_id=record.id, language_code=lang_code,\n source_language_detected=detected_lang, translated_value=val or \"\",\n final_value=val or \"\", status=\"translated\", needs_review=needs_review,\n ))\n return {\"successful\": successful, \"failed\": failed, \"skipped\": skipped, \"retries\": retries}\n\n @staticmethod\n def _extract_per_lang_values(td, target_languages):\n plv = {}\n has_any = False\n for lc in target_languages:\n lv = td.get(lc)\n if lv is not None and str(lv).strip():\n plv[lc] = str(lv)\n has_any = True\n if not has_any:\n t = td.get(\"translation\", \"\")\n if t.strip():\n plv[target_languages[0]] = t\n has_any = True\n return plv if has_any else {}\n\n def _add_skipped(self, row, run_id, batch_id, source_text, reason):\n self.db.add(TranslationRecord(\n id=str(uuid.uuid4()), batch_id=batch_id, run_id=run_id,\n source_sql=source_text, target_sql=\"\",\n source_object_type=\"table_row\", source_object_id=row.get(\"row_index\"),\n source_object_name=row.get(\"source_object_name\", \"\"),\n source_data=row.get(\"source_data\"), status=\"SKIPPED\",\n error_message=reason,\n ))\n\n # #region call_llm [C:3] [TYPE Function] [SEMANTICS translate, llm, call]\n # @BRIEF Route to provider-specific LLM call implementation.\n def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> tuple[str, str | None]:\n \"\"\"Call the configured LLM provider with the batch prompt.\"\"\"\n with belief_scope(\"LLMTranslationService.call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\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 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 model = provider.default_model or \"gpt-4o-mini\"\n provider_type = provider.provider_type.lower() if provider.provider_type else \"openai\"\n disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type not in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n raise ValueError(f\"Unsupported provider type '{provider_type}'\")\n return call_openai_compatible(\n base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt,\n provider_type=provider_type, max_tokens=max_tokens, disable_reasoning=disable_reasoning,\n )\n # #endregion call_llm\n\n # -- Static methods delegated to sub-modules for backward compat --\n @staticmethod\n def call_openai_compatible(*a, **kw):\n return call_openai_compatible(*a, **kw)\n\n @staticmethod\n def _parse_llm_response(*a, **kw):\n return parse_llm_response(*a, **kw)\n# #endregion LLMTranslationService\n# #endregion LLMTranslationService\n" }, { "contract_id": "call_llm_for_batch", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_call.py", - "start_line": 49, - "end_line": 85, + "start_line": 48, + "end_line": 84, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37034,8 +37895,8 @@ "contract_id": "call_llm", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_call.py", - "start_line": 278, - "end_line": 302, + "start_line": 277, + "end_line": 301, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37053,7 +37914,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/_llm_http.py", "start_line": 1, - "end_line": 189, + "end_line": 206, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37065,14 +37926,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMHttpClient [C:3] [TYPE Module] [SEMANTICS translate, llm, http, openai, retry, rate-limit]\n# @BRIEF HTTP client for OpenAI-compatible LLM API calls with rate-limit handling and\n# @RATIONALE Extracted 'openai' default provider type and 8192 default max_tokens into module-level constants DEFAULT_PROVIDER_TYPE and DEFAULT_MAX_TOKENS.\n\n# structured output fallback. Extracted from _llm_call.py for INV_7 compliance.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:requests]\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason) tuple.\n# @SIDE_EFFECT HTTP POST to LLM API with optional retry on 429.\n# @RATIONALE Extracted from LLMTranslationService (793 lines) to keep module under INV_7 limit.\n# @REJECTED Single HTTP client class — kept as module-level functions for stateless reusability.\n\nimport time\nfrom typing import Any\n\nfrom ...core.logger import logger\n\n\n# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\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 system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} 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\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"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 logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n\n\n# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n\n\n# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n# #endregion LLMHttpClient\n" + "body": "# #region LLMHttpClient [C:3] [TYPE Module] [SEMANTICS translate, llm, http, openai, retry, rate-limit]\n# @BRIEF HTTP client for OpenAI-compatible LLM API calls with rate-limit handling and\n# @RATIONALE Extracted 'openai' default provider type and 8192 default max_tokens into module-level constants DEFAULT_PROVIDER_TYPE and DEFAULT_MAX_TOKENS.\n\n# structured output fallback. Extracted from _llm_call.py for INV_7 compliance.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [EXT:requests]\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason) tuple.\n# @SIDE_EFFECT HTTP POST to LLM API with optional retry on 429.\n# @RATIONALE Extracted from LLMTranslationService (793 lines) to keep module under INV_7 limit.\n# @REJECTED Single HTTP client class — kept as module-level functions for stateless reusability.\n\nimport os\nimport time\nfrom typing import Any\n\nfrom ...core.logger import logger\n\n\n# #region _get_verify [C:1] [TYPE Function] [SEMANTICS translate, ssl, verify]\n# @BRIEF Resolve SSL verification path from LLM_SSL_VERIFY env var.\n# @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что\n# OpenSSL 3.x не использует intermediate CA сертификаты из cafile для\n# построения цепочки (verify code 20). capath с хеш-симлинками работает\n# корректно (verify code 0).\n# @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA\n# из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.\n# @POST Returns path to /etc/ssl/certs/ when enabled, False when disabled.\ndef _get_verify() -> str | bool:\n raw = os.getenv(\"LLM_SSL_VERIFY\", \"true\").strip().lower()\n if raw in (\"false\", \"0\", \"no\", \"off\"):\n return False\n return \"/etc/ssl/certs/\"\n# #endregion _get_verify\n\n\n# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} 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\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"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 logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n\n\n# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180, verify=_get_verify())\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n\n\n# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180, verify=_get_verify())\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n# #endregion LLMHttpClient\n" }, { "contract_id": "call_openai_compatible", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_http.py", - "start_line": 20, - "end_line": 135, + "start_line": 38, + "end_line": 152, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37086,14 +37947,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\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 system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} 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\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"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 logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n" + "body": "# #region call_openai_compatible [C:3] [TYPE Function] [SEMANTICS translate, llm, http, openai]\n# @BRIEF Call OpenAI-compatible API with rate-limit handling and structured output fallback.\n# @PRE Valid API endpoint, key, model, and prompt.\n# @POST Returns (response text, finish_reason).\n# @SIDE_EFFECT HTTP POST to LLM API.\ndef call_openai_compatible(\n base_url: str,\n api_key: str,\n model: str,\n prompt: str,\n provider_type: str = \"openai\",\n max_tokens: int = 8192,\n disable_reasoning: bool = False,\n) -> tuple[str, str | None]:\n \"\"\"Call OpenAI-compatible API for batch translation.\"\"\"\n if not base_url:\n raise ValueError(\"LLM provider has no base_url configured\")\n\n\n url = f\"{base_url.rstrip('/')}/chat/completions\"\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n system_content = (\n \"You are a database content translation assistant. \"\n \"Translate the provided text accurately, preserving data semantics. \"\n \"Respond directly with ONLY the JSON result. \"\n \"Do NOT include any reasoning, thinking, chain-of-thought, analysis, \"\n \"or explanation. Output ONLY valid JSON.\"\n )\n\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": system_content},\n {\"role\": \"user\", \"content\": prompt},\n ],\n \"temperature\": 0.1,\n \"max_tokens\": max_tokens,\n }\n\n if provider_type in (\"openai\", \"openai_compatible\", \"kilo\", \"openrouter\", \"litellm\"):\n if not disable_reasoning:\n payload[\"response_format\"] = {\"type\": \"json_object\"}\n\n if disable_reasoning:\n if provider_type not in (\"kilo\", \"openrouter\", \"litellm\"):\n payload[\"reasoning_effort\"] = \"none\"\n payload[\"max_tokens\"] = max_tokens\n\n logger.reason(\n f\"LLM request url={base_url} 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\n response, response_text = _do_http_request(url, headers, payload)\n _handle_response_format_fallback(response, response_text, payload, url, headers)\n\n if not response.ok:\n logger.explore(\n f\"LLM API error status={response.status_code} \"\n f\"model={payload.get('model')} \"\n f\"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 logger.explore(\"LLM returned no choices\", extra={\n \"src\": \"executor\", \"response_keys\": list(data.keys()),\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned no choices\")\n\n try:\n finish_reason = choices[0].get(\"finish_reason\") or \"none\"\n msg = choices[0].get(\"message\") or {}\n except (TypeError, AttributeError) as e:\n logger.explore(\"TypeError processing LLM response choices\", extra={\n \"src\": \"executor_diag\", \"error\": str(e),\n \"choices_0_type\": type(choices[0]).__name__ if choices else \"N/A\",\n \"choices_0_repr\": repr(choices[0])[:2000] if choices else \"N/A\",\n \"data_type\": type(data).__name__, \"data_preview\": str(data)[:2000],\n })\n raise ValueError(f\"LLM response processing failed: {e}\")\n\n refusal = msg.get(\"refusal\") if isinstance(msg, dict) else None\n if refusal:\n logger.explore(\"LLM refused to respond\", extra={\n \"src\": \"executor\", \"refusal\": str(refusal)[:500], \"finish_reason\": finish_reason,\n })\n raise ValueError(f\"LLM refused to respond: {refusal}\")\n\n content = msg.get(\"content\") if isinstance(msg, dict) else \"\"\n if not content and isinstance(msg, dict):\n content = msg.get(\"content\") or \"\"\n\n logger.reason(\n f\"LLM response finish_reason={finish_reason} content_len={len(content)} \"\n f\"msg_keys={list(msg.keys()) if isinstance(msg, dict) else []}\"\n )\n if not content:\n logger.explore(\"LLM returned empty content\", extra={\n \"src\": \"executor\", \"finish_reason\": finish_reason,\n \"msg_keys\": list(msg.keys()) if isinstance(msg, dict) else [],\n \"response_preview\": str(data)[:2000],\n })\n raise ValueError(\"LLM returned empty content\")\n\n return content, finish_reason\n# #endregion call_openai_compatible\n" }, { "contract_id": "_do_http_request", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_http.py", - "start_line": 138, - "end_line": 165, + "start_line": 155, + "end_line": 182, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37103,14 +37964,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n" + "body": "# #region _do_http_request [C:1] [TYPE Function]\ndef _do_http_request(url: str, headers: dict, payload: dict) -> tuple[Any, str]:\n \"\"\"Make HTTP POST with rate-limit (429) retry handling.\"\"\"\n import requests as http_requests\n _max_retry_429 = 3\n _retry_count_429 = 0\n while _retry_count_429 < _max_retry_429:\n response = http_requests.post(url, headers=headers, json=payload, timeout=180, verify=_get_verify())\n response_text = response.text\n if response.status_code == 429:\n _retry_count_429 += 1\n retry_after = response.headers.get(\"Retry-After\")\n if retry_after:\n try:\n wait = int(retry_after)\n except (ValueError, TypeError):\n wait = 2 ** _retry_count_429\n else:\n wait = 2 ** _retry_count_429\n logger.explore(f\"Rate limited (429), retry {_retry_count_429}/{_max_retry_429} after {wait}s\",\n extra={\"src\": \"executor\", \"retry_after\": retry_after, \"wait\": wait})\n time.sleep(wait)\n if _retry_count_429 >= _max_retry_429:\n break\n else:\n break\n return response, response_text\n# #endregion _do_http_request\n" }, { "contract_id": "_handle_response_format_fallback", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_http.py", - "start_line": 168, - "end_line": 188, + "start_line": 185, + "end_line": 205, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37120,14 +37981,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180)\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n" + "body": "# #region _handle_response_format_fallback [C:1] [TYPE Function]\ndef _handle_response_format_fallback(\n response: Any, response_text: str, payload: dict, url: str, headers: dict,\n) -> None:\n \"\"\"Handle 400 errors from structured_outputs not being supported.\"\"\"\n import requests as http_requests\n _patterns = (\"response_format\", \"structured_outputs\", \"structured\", \"json_object\")\n if (\n not response.ok\n and response.status_code == 400\n and any(p in (response_text or \"\").lower() for p in _patterns)\n ):\n logger.explore(\"Structured outputs not supported, retrying without response_format\",\n extra={\"src\": \"executor\"})\n payload.pop(\"response_format\", None)\n new_response = http_requests.post(url, headers=headers, json=payload, timeout=180, verify=_get_verify())\n response.status_code = new_response.status_code\n response._content = new_response.content\n response.encoding = new_response.encoding\n response.headers = new_response.headers\n# #endregion _handle_response_format_fallback\n" }, { "contract_id": "LLMResponseParser", "contract_type": "Module", "file_path": "backend/src/plugins/translate/_llm_parse.py", "start_line": 1, - "end_line": 117, + "end_line": 116, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -37138,14 +37999,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region LLMResponseParser [C:3] [TYPE Module] [SEMANTICS translate, llm, parse, json, recovery]\n# @BRIEF Parse LLM JSON response into per-row translations with support for markdown code\n# blocks, truncated JSON recovery, and multi-language format.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RATIONALE Extracted from _llm_call.py (793 lines) to comply with INV_7 (< 400 lines/module).\n# @REJECTED Inline parsing inside call_llm_for_batch — made the function 327 lines.\n\nimport json\nimport re\nfrom typing import Any\n\nfrom ...core.logger import logger\n\n\n# #region parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate, llm, parse, json]\n# @BRIEF Parse LLM JSON response into dict of row_id -> per-language translations.\n# @PRE response_text is valid JSON with {\"rows\": [...]} structure.\n# @POST Returns dict mapping row_id to dict with 'detected_source_language' and per-language codes.\n# @RAISES ValueError if JSON cannot be parsed and no rows recovered.\ndef parse_llm_response(\n response_text: str,\n expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n) -> dict[str, dict[str, str]]:\n \"\"\"Parse LLM JSON response into dict of row_id -> translations.\"\"\"\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n data = _recover_from_markdown(response_text)\n\n if data is None:\n data = _recover_truncated_rows(response_text, expected_count, finish_reason)\n\n if data is None:\n logger.explore(\"No complete rows found in truncated response\",\n extra={\"src\": \"executor\", \"response_preview\": response_text[:1000]})\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, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\"detected_source_language\": detected_lang}\n\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n has_language_data = True\n\n if has_language_data:\n translations[row_id] = result\n\n if len(translations) < expected_count:\n logger.explore(f\"LLM returned fewer translations expected={expected_count} got={len(translations)}\")\n\n return translations\n# #endregion parse_llm_response\n\n\n# #region _recover_from_markdown [C:1] [TYPE Function]\ndef _recover_from_markdown(response_text: str) -> dict | None:\n \"\"\"Try to extract JSON from markdown code block.\"\"\"\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 rows = data.get(\"rows\", [])\n if isinstance(rows, list) and rows:\n logger.reason(\"Parsed JSON from markdown code block\", {\"rows\": len(rows)})\n return data\n except json.JSONDecodeError:\n pass\n return None\n# #endregion _recover_from_markdown\n\n\n# #region _recover_truncated_rows [C:1] [TYPE Function]\ndef _recover_truncated_rows(\n response_text: str, expected_count: int, finish_reason: str | None,\n) -> dict | None:\n \"\"\"Try to recover complete rows from truncated JSON response.\"\"\"\n logger.explore(\"LLM truncated, trying partial row recovery\",\n extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"response_length\": len(response_text)})\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n row_data = json.loads(row_text)\n partial_rows.append(row_data)\n except json.JSONDecodeError:\n continue\n if partial_rows:\n logger.explore(f\"Recovered {len(partial_rows)}/{expected_count} complete rows from truncated response\",\n extra={\"src\": \"executor\"})\n return {\"rows\": partial_rows}\n return None\n# #endregion _recover_truncated_rows\n# #endregion LLMResponseParser\n" + "body": "# #region LLMResponseParser [C:3] [TYPE Module] [SEMANTICS translate, llm, parse, json, recovery]\n# @BRIEF Parse LLM JSON response into per-row translations with support for markdown code\n# blocks, truncated JSON recovery, and multi-language format.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RATIONALE Extracted from _llm_call.py (793 lines) to comply with INV_7 (< 400 lines/module).\n# @REJECTED Inline parsing inside call_llm_for_batch — made the function 327 lines.\n\nimport json\nimport re\n\nfrom ...core.logger import logger\n\n\n# #region parse_llm_response [C:3] [TYPE Function] [SEMANTICS translate, llm, parse, json]\n# @BRIEF Parse LLM JSON response into dict of row_id -> per-language translations.\n# @PRE response_text is valid JSON with {\"rows\": [...]} structure.\n# @POST Returns dict mapping row_id to dict with 'detected_source_language' and per-language codes.\n# @RAISES ValueError if JSON cannot be parsed and no rows recovered.\ndef parse_llm_response(\n response_text: str,\n expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n) -> dict[str, dict[str, str]]:\n \"\"\"Parse LLM JSON response into dict of row_id -> translations.\"\"\"\n try:\n data = json.loads(response_text)\n except json.JSONDecodeError:\n data = _recover_from_markdown(response_text)\n\n if data is None:\n data = _recover_truncated_rows(response_text, expected_count, finish_reason)\n\n if data is None:\n logger.explore(\"No complete rows found in truncated response\",\n extra={\"src\": \"executor\", \"response_preview\": response_text[:1000]})\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, dict[str, str]] = {}\n for item in rows:\n row_id = str(item.get(\"row_id\", \"\"))\n if not row_id:\n continue\n\n detected_lang = str(item.get(\"detected_source_language\", \"und\")) if item.get(\"detected_source_language\") else \"und\"\n result: dict[str, str] = {\"detected_source_language\": detected_lang}\n\n has_language_data = False\n if target_languages:\n for lang_code in target_languages:\n lang_val = item.get(lang_code)\n if lang_val is not None and str(lang_val).strip():\n result[lang_code] = str(lang_val)\n has_language_data = True\n\n if not has_language_data:\n translation = item.get(\"translation\")\n if translation is not None:\n result[\"translation\"] = str(translation)\n has_language_data = True\n\n if has_language_data:\n translations[row_id] = result\n\n if len(translations) < expected_count:\n logger.explore(f\"LLM returned fewer translations expected={expected_count} got={len(translations)}\")\n\n return translations\n# #endregion parse_llm_response\n\n\n# #region _recover_from_markdown [C:1] [TYPE Function]\ndef _recover_from_markdown(response_text: str) -> dict | None:\n \"\"\"Try to extract JSON from markdown code block.\"\"\"\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 rows = data.get(\"rows\", [])\n if isinstance(rows, list) and rows:\n logger.reason(\"Parsed JSON from markdown code block\", {\"rows\": len(rows)})\n return data\n except json.JSONDecodeError:\n pass\n return None\n# #endregion _recover_from_markdown\n\n\n# #region _recover_truncated_rows [C:1] [TYPE Function]\ndef _recover_truncated_rows(\n response_text: str, expected_count: int, finish_reason: str | None,\n) -> dict | None:\n \"\"\"Try to recover complete rows from truncated JSON response.\"\"\"\n logger.explore(\"LLM truncated, trying partial row recovery\",\n extra={\"src\": \"executor\", \"finish_reason\": finish_reason, \"response_length\": len(response_text)})\n rows_match = re.findall(r'\\{\\s*\"row_id\"\\s*:\\s*(?:\\d+|\"\\d+\").*?\\}\\s*', response_text, re.DOTALL)\n if rows_match:\n partial_rows = []\n for row_text in rows_match:\n try:\n row_data = json.loads(row_text)\n partial_rows.append(row_data)\n except json.JSONDecodeError:\n continue\n if partial_rows:\n logger.explore(f\"Recovered {len(partial_rows)}/{expected_count} complete rows from truncated response\",\n extra={\"src\": \"executor\"})\n return {\"rows\": partial_rows}\n return None\n# #endregion _recover_truncated_rows\n# #endregion LLMResponseParser\n" }, { "contract_id": "_recover_from_markdown", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_parse.py", - "start_line": 78, - "end_line": 92, + "start_line": 77, + "end_line": 91, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37161,8 +38022,8 @@ "contract_id": "_recover_truncated_rows", "contract_type": "Function", "file_path": "backend/src/plugins/translate/_llm_parse.py", - "start_line": 95, - "end_line": 116, + "start_line": 94, + "end_line": 115, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -37179,7 +38040,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/_run_service.py", "start_line": 1, - "end_line": 177, + "end_line": 175, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -37190,7 +38051,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region RunExecutionService [C:4] [TYPE Module] [SEMANTICS translate, run, execution, orchestration]\n# @BRIEF Full run lifecycle: prepare run, fetch source rows, filter new keys, orchestrate batches,\n# handle cancellation, update per-language stats, finalize run status.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun], [TranslationJob], [TranslationBatch], [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession], [BatchProcessingService], [AdaptiveBatchSizer]\n# @RELATION DEPENDS_ON -> [RunSourceFetcher]\n# @PRE DB session available. Run has valid job config.\n# @POST Run status updated to COMPLETED/FAILED/CANCELLED. Batches and records created.\n# @SIDE_EFFECT Fetches data from Superset API; calls BatchProcessingService; creates DB rows.\n# @RATIONALE Extracted from TranslationExecutor. Source fetching delegated to _run_source.py.\n# @REJECTED Keeping all run orchestration inside TranslationExecutor — caused class to exceed INV_7.\n\nfrom collections.abc import Callable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationJob, TranslationRecord, TranslationRun, TranslationRunLanguageStats\nfrom ._batch_sizer import AdaptiveBatchSizer\nfrom ._run_source import _extract_chart_data_rows, fetch_source_rows\n\n\n# #region RunExecutionService [C:4] [TYPE Class]\n# @BRIEF Orchestrate full translation run: fetch data, process batches, finalize.\nclass RunExecutionService:\n \"\"\"Orchestrate a full translation run: prepare, process batches, finalize.\"\"\"\n\n def __init__(\n self, db: Session, config_manager: ConfigManager,\n current_user: str | None = None,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n ) -> None:\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._preview_edits_cache: dict[str, dict[str, str]] | None = None\n\n # -- Source fetching (thin wrappers) --\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n return fetch_source_rows(self.db, self.config_manager, job_id, run_id)\n\n @staticmethod\n def _extract_chart_data_rows(response):\n return _extract_chart_data_rows(response)\n\n # -- Key/filter helpers --\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n \"\"\"Filter source rows to only new keys not yet translated.\"\"\"\n with belief_scope(\"RunExecutionService._filter_new_keys\"):\n prev_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job.id, TranslationRun.status == \"COMPLETED\",\n TranslationRun.insert_status == \"succeeded\", TranslationRun.id != run_id)\n .order_by(TranslationRun.created_at.desc()).first()\n )\n if not prev_run:\n logger.reason(\"No prior successful run — all keys treated as new\", {\"job_id\": job.id})\n return source_rows\n\n prev_records = (\n self.db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == prev_run.id, TranslationRecord.status == \"SUCCESS\")\n .all()\n )\n if not prev_records:\n return source_rows\n\n key_cols = job.target_key_cols or job.source_key_cols or []\n if not key_cols:\n logger.explore(\"No key columns configured — skipping new-key-only filter\", {\"job_id\": job.id})\n return source_rows\n\n existing_keys = set()\n for rec in prev_records:\n sd = rec.source_data or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n existing_keys.add(key_tuple)\n\n filtered = []\n skipped = 0\n for row in source_rows:\n sd = row.get(\"source_data\", {}) or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n if key_tuple not in existing_keys:\n filtered.append(row)\n else:\n skipped += 1\n\n logger.reason(f\"New-key-only filter: {len(source_rows)} total -> {len(filtered)} new, {skipped} skipped\",\n {\"job_id\": job.id, \"prev_run_id\": prev_run.id, \"key_cols\": key_cols})\n return filtered\n\n def _load_preview_edits(self, job_id: str) -> None:\n \"\"\"Load preview edits for carry-forward during execution.\"\"\"\n from ...models.translate import TranslationPreviewLanguage, TranslationPreviewRecord, TranslationPreviewSession\n\n with belief_scope(\"RunExecutionService._load_preview_edits\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"APPLIED\")\n .order_by(TranslationPreviewSession.created_at.desc()).first()\n )\n if not session:\n logger.reason(\"No applied preview session found — no edits to carry forward\", {\"job_id\": job_id})\n self._preview_edits_cache = {}\n return\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id).all()\n )\n edits: dict[str, dict[str, str]] = {}\n for rec in records:\n if not rec.source_data:\n continue\n key_hash = self._compute_key_hash(rec.source_data)\n edited_langs: dict[str, str] = {}\n for lang_entry in (rec.languages or []):\n if lang_entry.status in (\"edited\", \"approved\") and lang_entry.user_edit:\n edited_langs[lang_entry.language_code] = lang_entry.final_value or lang_entry.user_edit\n logger.reason(\"Carrying forward preview edit\", {\"key_hash\": key_hash, \"language_code\": lang_entry.language_code})\n if edited_langs:\n edits[key_hash] = edited_langs\n self._preview_edits_cache = edits\n logger.reason(f\"Loaded {len(edits)} preview edits for carry-forward\", {\"job_id\": job_id})\n\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n import hashlib\n import json\n return hashlib.sha256(json.dumps(source_data, sort_keys=True).encode()).hexdigest()[:16]\n\n # -- Language stats --\n def _update_language_stats_incremental(self, run_id: str, language_stats_map: dict[str, TranslationRunLanguageStats]) -> None:\n \"\"\"Update per-language statistics incrementally after each batch.\"\"\"\n with belief_scope(\"RunExecutionService._update_language_stats_incremental\"):\n records = self.db.query(TranslationRecord).filter(TranslationRecord.run_id == run_id).all()\n record_ids = [r.id for r in records]\n if not record_ids:\n return\n from ...models.translate import TranslationLanguage\n lang_entries = (\n self.db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids)).all()\n )\n from collections import defaultdict\n agg: dict[str, dict[str, int]] = defaultdict(lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_tokens = max(1, sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value) // 4)\n num_langs = len(language_stats_map) or 1\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n self.db.flush()\n# #endregion RunExecutionService\n# #endregion RunExecutionService\n" + "body": "# #region RunExecutionService [C:4] [TYPE Module] [SEMANTICS translate, run, execution, orchestration]\n# @BRIEF Full run lifecycle: prepare run, fetch source rows, filter new keys, orchestrate batches,\n# handle cancellation, update per-language stats, finalize run status.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRun], [TranslationJob], [TranslationBatch], [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession], [BatchProcessingService], [AdaptiveBatchSizer]\n# @RELATION DEPENDS_ON -> [RunSourceFetcher]\n# @PRE DB session available. Run has valid job config.\n# @POST Run status updated to COMPLETED/FAILED/CANCELLED. Batches and records created.\n# @SIDE_EFFECT Fetches data from Superset API; calls BatchProcessingService; creates DB rows.\n# @RATIONALE Extracted from TranslationExecutor. Source fetching delegated to _run_source.py.\n# @REJECTED Keeping all run orchestration inside TranslationExecutor — caused class to exceed INV_7.\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import TranslationRecord, TranslationRun, TranslationRunLanguageStats\nfrom ._run_source import _extract_chart_data_rows, fetch_source_rows\n\n\n# #region RunExecutionService [C:4] [TYPE Class]\n# @BRIEF Orchestrate full translation run: fetch data, process batches, finalize.\nclass RunExecutionService:\n \"\"\"Orchestrate a full translation run: prepare, process batches, finalize.\"\"\"\n\n def __init__(\n self, db: Session, config_manager: ConfigManager,\n current_user: str | None = None,\n on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,\n ) -> None:\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._preview_edits_cache: dict[str, dict[str, str]] | None = None\n\n # -- Source fetching (thin wrappers) --\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n return fetch_source_rows(self.db, self.config_manager, job_id, run_id)\n\n @staticmethod\n def _extract_chart_data_rows(response):\n return _extract_chart_data_rows(response)\n\n # -- Key/filter helpers --\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n \"\"\"Filter source rows to only new keys not yet translated.\"\"\"\n with belief_scope(\"RunExecutionService._filter_new_keys\"):\n prev_run = (\n self.db.query(TranslationRun)\n .filter(TranslationRun.job_id == job.id, TranslationRun.status == \"COMPLETED\",\n TranslationRun.insert_status == \"succeeded\", TranslationRun.id != run_id)\n .order_by(TranslationRun.created_at.desc()).first()\n )\n if not prev_run:\n logger.reason(\"No prior successful run — all keys treated as new\", {\"job_id\": job.id})\n return source_rows\n\n prev_records = (\n self.db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == prev_run.id, TranslationRecord.status == \"SUCCESS\")\n .all()\n )\n if not prev_records:\n return source_rows\n\n key_cols = job.target_key_cols or job.source_key_cols or []\n if not key_cols:\n logger.explore(\"No key columns configured — skipping new-key-only filter\", {\"job_id\": job.id})\n return source_rows\n\n existing_keys = set()\n for rec in prev_records:\n sd = rec.source_data or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n existing_keys.add(key_tuple)\n\n filtered = []\n skipped = 0\n for row in source_rows:\n sd = row.get(\"source_data\", {}) or {}\n key_tuple = tuple(str(sd.get(k, \"\")) for k in key_cols)\n if key_tuple not in existing_keys:\n filtered.append(row)\n else:\n skipped += 1\n\n logger.reason(f\"New-key-only filter: {len(source_rows)} total -> {len(filtered)} new, {skipped} skipped\",\n {\"job_id\": job.id, \"prev_run_id\": prev_run.id, \"key_cols\": key_cols})\n return filtered\n\n def _load_preview_edits(self, job_id: str) -> None:\n \"\"\"Load preview edits for carry-forward during execution.\"\"\"\n from ...models.translate import TranslationPreviewRecord, TranslationPreviewSession\n\n with belief_scope(\"RunExecutionService._load_preview_edits\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(TranslationPreviewSession.job_id == job_id, TranslationPreviewSession.status == \"APPLIED\")\n .order_by(TranslationPreviewSession.created_at.desc()).first()\n )\n if not session:\n logger.reason(\"No applied preview session found — no edits to carry forward\", {\"job_id\": job_id})\n self._preview_edits_cache = {}\n return\n\n records = (\n self.db.query(TranslationPreviewRecord)\n .filter(TranslationPreviewRecord.session_id == session.id).all()\n )\n edits: dict[str, dict[str, str]] = {}\n for rec in records:\n if not rec.source_data:\n continue\n key_hash = self._compute_key_hash(rec.source_data)\n edited_langs: dict[str, str] = {}\n for lang_entry in (rec.languages or []):\n if lang_entry.status in (\"edited\", \"approved\") and lang_entry.user_edit:\n edited_langs[lang_entry.language_code] = lang_entry.final_value or lang_entry.user_edit\n logger.reason(\"Carrying forward preview edit\", {\"key_hash\": key_hash, \"language_code\": lang_entry.language_code})\n if edited_langs:\n edits[key_hash] = edited_langs\n self._preview_edits_cache = edits\n logger.reason(f\"Loaded {len(edits)} preview edits for carry-forward\", {\"job_id\": job_id})\n\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n import hashlib\n import json\n return hashlib.sha256(json.dumps(source_data, sort_keys=True).encode()).hexdigest()[:16]\n\n # -- Language stats --\n def _update_language_stats_incremental(self, run_id: str, language_stats_map: dict[str, TranslationRunLanguageStats]) -> None:\n \"\"\"Update per-language statistics incrementally after each batch.\"\"\"\n with belief_scope(\"RunExecutionService._update_language_stats_incremental\"):\n records = self.db.query(TranslationRecord).filter(TranslationRecord.run_id == run_id).all()\n record_ids = [r.id for r in records]\n if not record_ids:\n return\n from ...models.translate import TranslationLanguage\n lang_entries = (\n self.db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids)).all()\n )\n from collections import defaultdict\n agg: dict[str, dict[str, int]] = defaultdict(lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_tokens = max(1, sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value) // 4)\n num_langs = len(language_stats_map) or 1\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n self.db.flush()\n# #endregion RunExecutionService\n# #endregion RunExecutionService\n" }, { "contract_id": "RunSourceFetcher", @@ -37548,7 +38409,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationUtils [C:3] [TYPE Module] [SEMANTICS translate, utils, hash, dictionary, cache]\n# @BRIEF Shared utility functions for the translation plugin — dictionary enforcement,\n# source hashing, cache lookup. Extracted from executor.py to break circular imports.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RATIONALE Extracted from TranslationExecutor to avoid circular imports when sub-services\n# (batch_proc, llm_call) need these helpers. The original monolithic executor.py\n# violated INV_7 at 1974 lines.\n\nimport hashlib\nimport json\nimport re\nfrom typing import Any\nimport unicodedata\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationLanguage, TranslationRecord\n\n\n# #region _normalize_term [TYPE Function]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup.\n# @RATIONALE NFC normalization is applied before lowercasing to ensure consistent\n# comparison of Unicode characters (e.g. precomposed vs decomposed forms).\n# @REJECTED Lowercasing without NFC normalization — would cause duplicate entries\n# for semantically identical Unicode strings in different normalization forms.\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\n\n# #region _detect_delimiter [TYPE Function]\n# @BRIEF Detect the delimiter used in a CSV/TSV header line.\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\n\n# #region _enforce_dictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionary, post-processing]\n# @BRIEF Post-process LLM output: enforce dictionary term replacements.\n# @PRE dict_matches is a list of dict entries with source_term/target_term.\n# @POST per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-process LLM output: enforce dictionary term replacements.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n if src_term.lower() not in text_lower:\n continue\n\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n if tgt_term.lower() in val.lower():\n continue\n\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n\n\n# #region _compute_source_hash [C:2] [TYPE Function] [SEMANTICS translate, hash, cache-key]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data,\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n\n\n# #region _check_translation_cache [C:2] [TYPE Function] [SEMANTICS translate, cache, lookup]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n\n\n# #region _compute_key_hash [C:1] [TYPE Function]\n# @BRIEF Compute a stable hash from source_data dict for matching preview edits.\ndef _compute_key_hash(source_data: dict) -> str:\n \"\"\"Compute a stable hash from source_data dict for matching preview edits.\"\"\"\n import hashlib\n stable = json.dumps(source_data, sort_keys=True)\n return hashlib.sha256(stable.encode()).hexdigest()[:16]\n# #endregion _compute_key_hash\n\n\n# #region estimate_row_tokens [C:2] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n# #endregion TranslationUtils\n" + "body": "# #region TranslationUtils [C:3] [TYPE Module] [SEMANTICS translate, utils, hash, dictionary, cache]\n# @BRIEF Shared utility functions for the translation plugin — dictionary enforcement,\n# source hashing, cache lookup. Extracted from executor.py to break circular imports.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RATIONALE Extracted from TranslationExecutor to avoid circular imports when sub-services\n# (batch_proc, llm_call) need these helpers. The original monolithic executor.py\n# violated INV_7 at 1974 lines.\n\nimport hashlib\nimport json\nimport re\nfrom typing import Any\nimport unicodedata\n\nfrom sqlalchemy.orm import Session, joinedload\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationRecord\n\n\n# #region _normalize_term [TYPE Function]\n# @BRIEF Normalize a term for case-insensitive unique constraint lookup.\n# @RATIONALE NFC normalization is applied before lowercasing to ensure consistent\n# comparison of Unicode characters (e.g. precomposed vs decomposed forms).\n# @REJECTED Lowercasing without NFC normalization — would cause duplicate entries\n# for semantically identical Unicode strings in different normalization forms.\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\n\n# #region _detect_delimiter [TYPE Function]\n# @BRIEF Detect the delimiter used in a CSV/TSV header line.\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\n\n# #region _enforce_dictionary [C:2] [TYPE Function] [SEMANTICS translate, dictionary, post-processing]\n# @BRIEF Post-process LLM output: enforce dictionary term replacements.\n# @PRE dict_matches is a list of dict entries with source_term/target_term.\n# @POST per_lang_values may be mutated to include forced dictionary replacements.\n# @SIDE_EFFECT Logs when enforcement is applied.\ndef _enforce_dictionary(\n source_text: str,\n per_lang_values: dict[str, str],\n dict_matches: list[dict[str, Any]],\n batch_id: str,\n row_id: str,\n) -> None:\n \"\"\"Post-process LLM output: enforce dictionary term replacements.\n\n For each dictionary entry whose source_term appears in the source_text,\n verify that the target_term is present in each language's translation.\n If missing, replace any occurrence of the source term (which the LLM\n may have left untranslated) with the dictionary target term.\n \"\"\"\n if not dict_matches or not source_text:\n return\n\n text_lower = source_text.lower()\n\n for dm in dict_matches:\n src_term = dm.get(\"source_term\", \"\")\n tgt_term = dm.get(\"target_term\", \"\")\n if not src_term or not tgt_term:\n continue\n\n if src_term.lower() not in text_lower:\n continue\n\n for lang_code in list(per_lang_values.keys()):\n val = per_lang_values[lang_code]\n if not val:\n continue\n\n if tgt_term.lower() in val.lower():\n continue\n\n src_pattern = re.compile(re.escape(src_term), re.IGNORECASE)\n if src_pattern.search(val):\n new_val = src_pattern.sub(lambda _: tgt_term, val)\n if new_val != val:\n logger.reason(\"Dictionary enforcement applied\", {\n \"batch_id\": batch_id,\n \"row_id\": row_id,\n \"language_code\": lang_code,\n \"source_term\": src_term,\n \"target_term\": tgt_term,\n \"before\": val[:200],\n \"after\": new_val[:200],\n })\n per_lang_values[lang_code] = new_val\n# #endregion _enforce_dictionary\n\n\n# #region _compute_source_hash [C:2] [TYPE Function] [SEMANTICS translate, hash, cache-key]\n# @BRIEF Compute deterministic cache key for a source row.\n# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash).\ndef _compute_source_hash(\n source_text: str,\n source_data: dict | None,\n dict_snapshot_hash: str | None,\n config_hash: str | None,\n context_keys: list[str] | None = None,\n) -> str:\n \"\"\"Deterministic cache key for a translation source row.\n\n Only includes source_text and context-relevant fields from source_data.\n Key/identifier columns are excluded so identical text in different rows hits cache.\n \"\"\"\n context_data: dict[str, str] = {}\n if source_data and context_keys:\n for key in context_keys:\n val = source_data.get(key)\n if val is not None:\n context_data[key] = str(val)\n\n payload = json.dumps({\n \"text\": source_text,\n \"ctx\": context_data,\n \"dict_hash\": dict_snapshot_hash or \"\",\n \"config_hash\": config_hash or \"\",\n }, sort_keys=True, ensure_ascii=False)\n return hashlib.sha256(payload.encode(\"utf-8\")).hexdigest()\n# #endregion _compute_source_hash\n\n\n# #region _check_translation_cache [C:2] [TYPE Function] [SEMANTICS translate, cache, lookup]\n# @BRIEF Look up a previously successful translation by source_hash.\n# Returns per-language dict {lang_code: final_value} or None.\ndef _check_translation_cache(\n db: Session,\n source_hash: str,\n) -> dict[str, str] | None:\n \"\"\"Check if this source_hash was already translated successfully.\"\"\"\n cached = (\n db.query(TranslationRecord)\n .options(joinedload(TranslationRecord.languages))\n .filter(\n TranslationRecord.source_hash == source_hash,\n TranslationRecord.status == \"SUCCESS\",\n )\n .order_by(TranslationRecord.created_at.desc())\n .first()\n )\n if not cached:\n return None\n\n lang_values: dict[str, str] = {}\n for lang in cached.languages:\n if lang.status == \"translated\" and lang.final_value:\n lang_values[lang.language_code] = lang.final_value\n\n return lang_values if lang_values else None\n# #endregion _check_translation_cache\n\n\n# #region _compute_key_hash [C:1] [TYPE Function]\n# @BRIEF Compute a stable hash from source_data dict for matching preview edits.\ndef _compute_key_hash(source_data: dict) -> str:\n \"\"\"Compute a stable hash from source_data dict for matching preview edits.\"\"\"\n import hashlib\n stable = json.dumps(source_data, sort_keys=True)\n return hashlib.sha256(stable.encode()).hexdigest()[:16]\n# #endregion _compute_key_hash\n\n\n# #region estimate_row_tokens [C:2] [TYPE Function] [SEMANTICS translate, token, estimation]\n# @BRIEF Estimate token count for a single source row including context fields.\n# @PRE source_text is a string.\n# @POST Returns estimated token count >= 1.\ndef estimate_row_tokens(\n source_text: str,\n source_data: dict | None,\n job,\n) -> int:\n \"\"\"Estimate token count for a single source row including context fields.\n\n Uses CJK-aware heuristics via _token_budget._estimate_tokens_for_text.\n Context fields from job.context_columns are included in the estimate.\n \"\"\"\n from ._token_budget import _estimate_tokens_for_text\n\n text_tokens = _estimate_tokens_for_text(source_text or \"\")\n\n context_keys = job.context_columns or []\n ctx_text = \"\"\n if source_data and context_keys:\n ctx_text = \" \".join(str(source_data.get(k, \"\")) for k in context_keys)\n ctx_tokens = _estimate_tokens_for_text(ctx_text)\n\n return text_tokens + ctx_tokens\n# #endregion estimate_row_tokens\n# #endregion TranslationUtils\n" }, { "contract_id": "_normalize_term", @@ -37685,7 +38546,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/dictionary.py", "start_line": 1, - "end_line": 68, + "end_line": 63, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -37727,14 +38588,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryManagerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, dictionary, batch, term]\n# @BRIEF Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n# @RATIONALE C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion.\n# @REJECTED \"Keep both\" as conflict option — UniqueConstraint prohibits variants; only overwrite/keep existing.\n# @REJECTED Monolithic DictionaryManager class — violated INV_7. Decomposed into DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @PRE Database session is open and valid.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import DictionaryEntry, TerminologyDictionary\nfrom .dictionary_correction import DictionaryCorrectionService\nfrom .dictionary_crud import DictionaryCRUD\nfrom .dictionary_entries import DictionaryEntryCRUD\nfrom .dictionary_filter import DictionaryBatchFilter\nfrom .dictionary_import_export import DictionaryImportExport\nfrom .dictionary_validation import _validate_bcp47\n\n\n# #region DictionaryManager [C:4] [TYPE Class]\n# @BRIEF Facade for terminology dictionaries: delegates to DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @PRE Database session is open and valid.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n# @RATIONALE Thin facade preserves API compatibility after decomposition.\n# @REJECTED Removing the facade would break all upstream imports; delegation pattern preferred.\nclass DictionaryManager:\n \"\"\"Facade for terminology dictionary operations — delegates to specialized sub-classes.\"\"\"\n\n # Dictionary CRUD\n create_dictionary = DictionaryCRUD.create_dictionary\n update_dictionary = DictionaryCRUD.update_dictionary\n delete_dictionary = DictionaryCRUD.delete_dictionary\n get_dictionary = DictionaryCRUD.get_dictionary\n list_dictionaries = DictionaryCRUD.list_dictionaries\n\n # Entry CRUD\n add_entry = DictionaryEntryCRUD.add_entry\n edit_entry = DictionaryEntryCRUD.edit_entry\n delete_entry = DictionaryEntryCRUD.delete_entry\n clear_entries = DictionaryEntryCRUD.clear_entries\n list_entries = DictionaryEntryCRUD.list_entries\n\n # Import/Export\n import_entries = DictionaryImportExport.import_entries\n export_entries = DictionaryImportExport.export_entries\n migrate_old_entries = DictionaryImportExport.migrate_old_entries\n\n # Batch filtering\n filter_for_batch = DictionaryBatchFilter.filter_for_batch\n\n # Corrections\n submit_correction = DictionaryCorrectionService.submit_correction\n submit_bulk_corrections = DictionaryCorrectionService.submit_bulk_corrections\n\n# #endregion DictionaryManager\n\n\n# #endregion DictionaryManagerModule\n" + "body": "# #region DictionaryManagerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, dictionary, batch, term]\n# @BRIEF Business logic for terminology dictionary management, entry CRUD, CSV/TSV import with conflict detection, and per-batch filtering.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [TerminologyDictionary]\n# @RELATION DEPENDS_ON -> [TranslationJobDictionary]\n# @RATIONALE C4 complexity because dictionary CRUD is stateful with referential integrity enforcement on deletion.\n# @REJECTED \"Keep both\" as conflict option — UniqueConstraint prohibits variants; only overwrite/keep existing.\n# @REJECTED Monolithic DictionaryManager class — violated INV_7. Decomposed into DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @PRE Database session is open and valid.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n\n\n\nfrom .dictionary_correction import DictionaryCorrectionService\nfrom .dictionary_crud import DictionaryCRUD\nfrom .dictionary_entries import DictionaryEntryCRUD\nfrom .dictionary_filter import DictionaryBatchFilter\nfrom .dictionary_import_export import DictionaryImportExport\n\n\n# #region DictionaryManager [C:4] [TYPE Class]\n# @BRIEF Facade for terminology dictionaries: delegates to DictionaryCRUD, DictionaryEntryCRUD, DictionaryImportExport, DictionaryBatchFilter, DictionaryCorrectionService.\n# @PRE Database session is open and valid.\n# @POST Dictionary and entry mutations are persisted with conflict detection.\n# @SIDE_EFFECT Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.\n# @RATIONALE Thin facade preserves API compatibility after decomposition.\n# @REJECTED Removing the facade would break all upstream imports; delegation pattern preferred.\nclass DictionaryManager:\n \"\"\"Facade for terminology dictionary operations — delegates to specialized sub-classes.\"\"\"\n\n # Dictionary CRUD\n create_dictionary = DictionaryCRUD.create_dictionary\n update_dictionary = DictionaryCRUD.update_dictionary\n delete_dictionary = DictionaryCRUD.delete_dictionary\n get_dictionary = DictionaryCRUD.get_dictionary\n list_dictionaries = DictionaryCRUD.list_dictionaries\n\n # Entry CRUD\n add_entry = DictionaryEntryCRUD.add_entry\n edit_entry = DictionaryEntryCRUD.edit_entry\n delete_entry = DictionaryEntryCRUD.delete_entry\n clear_entries = DictionaryEntryCRUD.clear_entries\n list_entries = DictionaryEntryCRUD.list_entries\n\n # Import/Export\n import_entries = DictionaryImportExport.import_entries\n export_entries = DictionaryImportExport.export_entries\n migrate_old_entries = DictionaryImportExport.migrate_old_entries\n\n # Batch filtering\n filter_for_batch = DictionaryBatchFilter.filter_for_batch\n\n # Corrections\n submit_correction = DictionaryCorrectionService.submit_correction\n submit_bulk_corrections = DictionaryCorrectionService.submit_bulk_corrections\n\n# #endregion DictionaryManager\n\n\n# #endregion DictionaryManagerModule\n" }, { "contract_id": "DictionaryManager", "contract_type": "Class", "file_path": "backend/src/plugins/translate/dictionary.py", - "start_line": 29, - "end_line": 65, + "start_line": 24, + "end_line": 60, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -37987,7 +38848,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/executor.py", "start_line": 1, - "end_line": 256, + "end_line": 258, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -37998,14 +38859,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationExecutor [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, orchestration]\n# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch\n# and TranslationRecord rows. Thin orchestrator — delegates to focused sub-services.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationRun]\n# @RELATION DEPENDS_ON -> [LLMProviderService], [DictionaryManager], [TranslationPreview]\n# @RELATION DEPENDS_ON -> [RunExecutionService], [BatchProcessingService]\n# @RELATION DEPENDS_ON -> [LLMTranslationService], [AdaptiveBatchSizer]\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: updated Run with batch/record rows\n# @INVARIANT Batch processing is independent — one batch failure does not affect others.\n# @RATIONALE Extracted from monolithic executor.py (1974 lines) into thin orchestrator\n# to comply with INV_7. Sub-services in _run_service, _batch_proc, _llm_call, _batch_sizer.\n# Module-level helpers moved to _utils.py.\n# @REJECTED Keeping monolithic executor.py at 1974 lines — violates INV_7 by +1574 lines.\n# Single monolithic LLM call — would lose all progress on any failure.\n\nfrom collections.abc import Callable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats\nfrom ...services.llm_provider import LLMProviderService\nfrom ._run_service import RunExecutionService\nfrom ._token_budget import estimate_token_budget\nfrom ._utils import _check_translation_cache, _compute_source_hash, _enforce_dictionary, estimate_row_tokens\n\n__all__ = [\n \"TranslationExecutor\", \"estimate_row_tokens\",\n \"_enforce_dictionary\", \"_compute_source_hash\", \"_check_translation_cache\",\n]\n\n\nclass TranslationExecutor:\n \"\"\"Process translation batches. Thin orchestrator delegating to sub-services.\"\"\"\n\n def __init__(\n self, db: Session, config_manager: ConfigManager,\n current_user: str | None = None,\n on_batch_progress: Callable[[str, int, int, int, int], None] | 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._preview_edits_cache: dict[str, dict[str, str]] | None = None\n self._run_service = RunExecutionService(db, config_manager, current_user, on_batch_progress)\n\n # region execute_run [C:3] [TYPE Function] [SEMANTICS translate, run, orchestrate]\n # @BRIEF Run full translation execution. Logic on self for test-patch compatibility.\n # @PRE run is in PENDING or RUNNING status with valid job config.\n # @POST Run is populated with batches and records.\n # @SIDE_EFFECT LLM API calls; DB batch writes.\n def execute_run(\n self, run: TranslationRun,\n llm_progress_callback: Callable[[str, int, int, int], None] | None = None,\n language_stats_map: dict[str, TranslationRunLanguageStats] | None = None,\n ) -> TranslationRun:\n \"\"\"Execute full translation run. Logic on self for test-patch compatibility.\"\"\"\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 logger.reason(\"Starting translation execution\", {\"run_id\": run.id, \"job_id\": job.id})\n\n run, source_rows, job = self._prepare_run(run, job)\n if run.status != \"RUNNING\":\n return run\n\n target_languages, batches = self._prepare_batches(job, source_rows, run.id)\n run.total_records = len(source_rows)\n logger.reason(f\"Processing {len(batches)} batches\", {\"run_id\": run.id, \"total_rows\": run.total_records})\n\n return self._process_batches(run, job, batches, target_languages, language_stats_map)\n # endregion execute_run\n\n # region _prepare_run [C:2] [TYPE Function]\n def _prepare_run(self, run: TranslationRun, job: TranslationJob) -> tuple:\n \"\"\"Prepare run: load preview edits, fetch source rows, filter new keys.\"\"\"\n self._load_preview_edits(job.id)\n run.status = \"RUNNING\"\n run.started_at = datetime.now(UTC)\n self.db.flush()\n\n full_translation = False\n if run.config_snapshot and isinstance(run.config_snapshot, dict):\n full_translation = run.config_snapshot.get(\"full_translation\", False)\n\n source_rows = self._fetch_source_rows(job.id, run.id)\n if not source_rows:\n logger.explore(\"No source rows to translate\", {\"run_id\": run.id})\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n return run, [], job\n\n if run.trigger_type == \"new_key_only\" or (not full_translation and run.trigger_type == \"manual\"):\n source_rows = self._filter_new_keys(job, run.id, source_rows)\n if not source_rows:\n logger.reason(\"run_noop — no new rows to translate\", {\"job_id\": job.id, \"run_id\": run.id})\n run.status = \"COMPLETED\"\n run.insert_status = \"skipped\"\n run.total_records = 0\n run.completed_at = datetime.now(UTC)\n self.db.commit()\n return run, [], job\n return run, source_rows, job\n # endregion _prepare_run\n\n # region _prepare_batches [C:2] [TYPE Function]\n def _prepare_batches(self, job: TranslationJob, source_rows: list, run_id: str) -> tuple:\n \"\"\"Resolve target languages and create auto-sized batches.\"\"\"\n target_languages = job.target_languages or [job.target_dialect or \"en\"]\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n batches = self._auto_size_batches(job=job, source_rows=source_rows, target_languages=target_languages)\n return target_languages, batches\n # endregion _prepare_batches\n\n # region _process_batches [C:3] [TYPE Function]\n def _process_batches(self, run: TranslationRun, job: TranslationJob, batches: list,\n target_languages: list[str],\n language_stats_map: dict[str, TranslationRunLanguageStats] | None = None) -> TranslationRun:\n \"\"\"Process all batches: execute, insert to target, check cancellation.\"\"\"\n successful_records = failed_records = skipped_records = 0\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job, run_id=run.id, batch_index=batch_idx, batch_rows=batch_rows,\n dict_snapshot_hash=run.dict_snapshot_hash, config_hash=run.config_hash,\n )\n successful_records += batch_result.get(\"successful\", 0)\n failed_records += batch_result.get(\"failed\", 0)\n skipped_records += batch_result.get(\"skipped\", 0)\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n self.db.commit()\n\n batch_id = batch_result.get(\"batch_id\")\n if batch_id and batch_result.get(\"successful\", 0) > 0:\n try:\n self._insert_batch_to_target(job, batch_id, run.id)\n except Exception as e:\n logger.explore(\"Batch INSERT failed (non-fatal)\", {\"batch_id\": batch_id, \"error\": str(e)})\n\n self.db.refresh(run)\n if run.error_message == \"CANCEL_REQUESTED\":\n logger.reason(\"Cancellation requested — stopping\", {\"run_id\": run.id, \"batch_index\": batch_idx})\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(UTC)\n run.error_message = None\n self.db.commit()\n return run\n\n if language_stats_map and batch_result.get(\"successful\", 0) > 0:\n try:\n self._update_language_stats_incremental(run.id, language_stats_map)\n except Exception as e:\n logger.explore(\"Language stats update failed (non-fatal)\", {\"batch_id\": batch_id, \"error\": str(e)})\n if self.on_batch_progress:\n self.on_batch_progress(run.id, batch_idx + 1, len(batches), successful_records, run.total_records or 0)\n\n return self._finalize_run(run, successful_records, failed_records, skipped_records)\n # endregion _process_batches\n\n @staticmethod\n def _finalize_run(run: TranslationRun, s: int, f: int, sk: int) -> TranslationRun:\n if f == 0 and sk == 0:\n run.status = \"COMPLETED\"\n elif s == 0:\n run.status = \"FAILED\"\n run.error_message = f\"All {f} record(s) failed — see individual record errors for details\"\n else:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n logger.reflect(\"Translation execution complete\", {\"run_id\": run.id, \"status\": run.status,\n \"successful\": s, \"failed\": f, \"skipped\": sk})\n return run\n\n # -- Delegation methods (thin wrappers for test-patch compatibility) --\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n return self._run_service._fetch_source_rows(job_id, run_id)\n\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n return self._run_service._filter_new_keys(job, run_id, source_rows)\n\n @staticmethod\n def _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n return RunExecutionService._extract_chart_data_rows(response)\n\n def _load_preview_edits(self, job_id: str) -> None:\n self._run_service._load_preview_edits(job_id)\n self._preview_edits_cache = self._run_service._preview_edits_cache\n\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n return RunExecutionService._compute_key_hash(source_data)\n\n def _update_language_stats_incremental(self, run_id: str, language_stats_map) -> None:\n self._run_service._update_language_stats_incremental(run_id, language_stats_map)\n\n def _resolve_provider_model(self, job) -> str | None:\n \"\"\"Resolve the LLM provider model name for token budget estimation.\"\"\"\n if not job.provider_id:\n return None\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.get_provider(job.provider_id)\n if p:\n return p.default_model or \"gpt-4o-mini\"\n except Exception:\n pass\n return None\n\n def _auto_size_batches(self, job, source_rows, target_languages, provider_info=None) -> list:\n \"\"\"Split source rows into auto-sized batches based on content length.\"\"\"\n from ._batch_sizer import AdaptiveBatchSizer\n if provider_info is None:\n provider_info = self._resolve_provider_model(job)\n return AdaptiveBatchSizer(self.db, self.config_manager).auto_size_batches(\n job, source_rows, target_languages, provider_info)\n\n def _process_batch(self, job, run_id, batch_index, batch_rows, dict_snapshot_hash=None, config_hash=None) -> dict:\n from ._batch_proc import BatchProcessingService\n return BatchProcessingService(self.db, self.config_manager).process_batch(\n job, run_id, batch_index, batch_rows, dict_snapshot_hash, config_hash,\n preview_edits_cache=self._preview_edits_cache)\n\n def _insert_batch_to_target(self, job, batch_id, run_id) -> None:\n from ._batch_proc import BatchProcessingService\n BatchProcessingService(self.db, self.config_manager).insert_batch_to_target(job, batch_id, run_id)\n\n def _call_llm_for_batch(self, job, run_id, batch_rows, dict_matches, batch_id, max_tokens=8192, _recursion_depth=0) -> dict:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService(self.db).call_llm_for_batch(\n job, run_id, batch_rows, dict_matches, batch_id, max_tokens, _recursion_depth)\n\n def _call_llm(self, job, prompt, max_tokens=8192) -> tuple:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService(self.db).call_llm(job, prompt, max_tokens)\n\n @staticmethod\n def _call_openai_compatible(base_url, api_key, model, prompt, provider_type=\"openai\", max_tokens=8192, disable_reasoning=False) -> tuple:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService.call_openai_compatible(base_url, api_key, model, prompt, provider_type, max_tokens, disable_reasoning)\n\n @staticmethod\n def _parse_llm_response(response_text, expected_count, target_languages=None, finish_reason=None) -> dict:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService._parse_llm_response(response_text, expected_count, target_languages, finish_reason)\n# #endregion TranslationExecutor\n" + "body": "# #region TranslationExecutor [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, orchestration]\n# @BRIEF Process translation in batches: fetch source rows, call LLM, persist TranslationBatch\n# and TranslationRecord rows. Thin orchestrator — delegates to focused sub-services.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationBatch], [TranslationRecord], [TranslationRun]\n# @RELATION DEPENDS_ON -> [LLMProviderService], [DictionaryManager], [TranslationPreview]\n# @RELATION DEPENDS_ON -> [RunExecutionService], [BatchProcessingService]\n# @RELATION DEPENDS_ON -> [LLMTranslationService], [AdaptiveBatchSizer]\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: updated Run with batch/record rows\n# @INVARIANT Batch processing is independent — one batch failure does not affect others.\n# @RATIONALE Extracted from monolithic executor.py (1974 lines) into thin orchestrator\n# to comply with INV_7. Sub-services in _run_service, _batch_proc, _llm_call, _batch_sizer.\n# Module-level helpers moved to _utils.py.\n# @REJECTED Keeping monolithic executor.py at 1974 lines — violates INV_7 by +1574 lines.\n# Single monolithic LLM call — would lose all progress on any failure.\n\nfrom collections.abc import Callable\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats\nfrom ...services.llm_provider import LLMProviderService\nfrom ._run_service import RunExecutionService\nfrom ._utils import _check_translation_cache, _compute_source_hash, _enforce_dictionary, estimate_row_tokens\n\n__all__ = [\n \"TranslationExecutor\",\n \"_check_translation_cache\",\n \"_compute_source_hash\",\n \"_enforce_dictionary\",\n \"estimate_row_tokens\",\n]\n\n\nclass TranslationExecutor:\n \"\"\"Process translation batches. Thin orchestrator delegating to sub-services.\"\"\"\n\n def __init__(\n self, db: Session, config_manager: ConfigManager,\n current_user: str | None = None,\n on_batch_progress: Callable[[str, int, int, int, int], None] | 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._preview_edits_cache: dict[str, dict[str, str]] | None = None\n self._run_service = RunExecutionService(db, config_manager, current_user, on_batch_progress)\n\n # region execute_run [C:3] [TYPE Function] [SEMANTICS translate, run, orchestrate]\n # @BRIEF Run full translation execution. Logic on self for test-patch compatibility.\n # @PRE run is in PENDING or RUNNING status with valid job config.\n # @POST Run is populated with batches and records.\n # @SIDE_EFFECT LLM API calls; DB batch writes.\n def execute_run(\n self, run: TranslationRun,\n llm_progress_callback: Callable[[str, int, int, int], None] | None = None,\n language_stats_map: dict[str, TranslationRunLanguageStats] | None = None,\n ) -> TranslationRun:\n \"\"\"Execute full translation run. Logic on self for test-patch compatibility.\"\"\"\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 logger.reason(\"Starting translation execution\", {\"run_id\": run.id, \"job_id\": job.id})\n\n run, source_rows, job = self._prepare_run(run, job)\n if run.status != \"RUNNING\":\n return run\n\n target_languages, batches = self._prepare_batches(job, source_rows, run.id)\n run.total_records = len(source_rows)\n logger.reason(f\"Processing {len(batches)} batches\", {\"run_id\": run.id, \"total_rows\": run.total_records})\n\n return self._process_batches(run, job, batches, target_languages, language_stats_map)\n # endregion execute_run\n\n # region _prepare_run [C:2] [TYPE Function]\n def _prepare_run(self, run: TranslationRun, job: TranslationJob) -> tuple:\n \"\"\"Prepare run: load preview edits, fetch source rows, filter new keys.\"\"\"\n self._load_preview_edits(job.id)\n run.status = \"RUNNING\"\n run.started_at = datetime.now(UTC)\n self.db.flush()\n\n full_translation = False\n if run.config_snapshot and isinstance(run.config_snapshot, dict):\n full_translation = run.config_snapshot.get(\"full_translation\", False)\n\n source_rows = self._fetch_source_rows(job.id, run.id)\n if not source_rows:\n logger.explore(\"No source rows to translate\", {\"run_id\": run.id})\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n self.db.flush()\n return run, [], job\n\n if run.trigger_type == \"new_key_only\" or (not full_translation and run.trigger_type == \"manual\"):\n source_rows = self._filter_new_keys(job, run.id, source_rows)\n if not source_rows:\n logger.reason(\"run_noop — no new rows to translate\", {\"job_id\": job.id, \"run_id\": run.id})\n run.status = \"COMPLETED\"\n run.insert_status = \"skipped\"\n run.total_records = 0\n run.completed_at = datetime.now(UTC)\n self.db.commit()\n return run, [], job\n return run, source_rows, job\n # endregion _prepare_run\n\n # region _prepare_batches [C:2] [TYPE Function]\n def _prepare_batches(self, job: TranslationJob, source_rows: list, run_id: str) -> tuple:\n \"\"\"Resolve target languages and create auto-sized batches.\"\"\"\n target_languages = job.target_languages or [job.target_dialect or \"en\"]\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n batches = self._auto_size_batches(job=job, source_rows=source_rows, target_languages=target_languages)\n return target_languages, batches\n # endregion _prepare_batches\n\n # region _process_batches [C:3] [TYPE Function]\n def _process_batches(self, run: TranslationRun, job: TranslationJob, batches: list,\n target_languages: list[str],\n language_stats_map: dict[str, TranslationRunLanguageStats] | None = None) -> TranslationRun:\n \"\"\"Process all batches: execute, insert to target, check cancellation.\"\"\"\n successful_records = failed_records = skipped_records = 0\n for batch_idx, batch_rows in enumerate(batches):\n batch_result = self._process_batch(\n job=job, run_id=run.id, batch_index=batch_idx, batch_rows=batch_rows,\n dict_snapshot_hash=run.dict_snapshot_hash, config_hash=run.config_hash,\n )\n successful_records += batch_result.get(\"successful\", 0)\n failed_records += batch_result.get(\"failed\", 0)\n skipped_records += batch_result.get(\"skipped\", 0)\n run.successful_records = successful_records\n run.failed_records = failed_records\n run.skipped_records = skipped_records\n self.db.commit()\n\n batch_id = batch_result.get(\"batch_id\")\n if batch_id and batch_result.get(\"successful\", 0) > 0:\n try:\n self._insert_batch_to_target(job, batch_id, run.id)\n except Exception as e:\n logger.explore(\"Batch INSERT failed (non-fatal)\", {\"batch_id\": batch_id, \"error\": str(e)})\n\n self.db.refresh(run)\n if run.error_message == \"CANCEL_REQUESTED\":\n logger.reason(\"Cancellation requested — stopping\", {\"run_id\": run.id, \"batch_index\": batch_idx})\n run.status = \"CANCELLED\"\n run.completed_at = datetime.now(UTC)\n run.error_message = None\n self.db.commit()\n return run\n\n if language_stats_map and batch_result.get(\"successful\", 0) > 0:\n try:\n self._update_language_stats_incremental(run.id, language_stats_map)\n except Exception as e:\n logger.explore(\"Language stats update failed (non-fatal)\", {\"batch_id\": batch_id, \"error\": str(e)})\n if self.on_batch_progress:\n self.on_batch_progress(run.id, batch_idx + 1, len(batches), successful_records, run.total_records or 0)\n\n return self._finalize_run(run, successful_records, failed_records, skipped_records)\n # endregion _process_batches\n\n @staticmethod\n def _finalize_run(run: TranslationRun, s: int, f: int, sk: int) -> TranslationRun:\n if f == 0 and sk == 0:\n run.status = \"COMPLETED\"\n elif s == 0:\n run.status = \"FAILED\"\n run.error_message = f\"All {f} record(s) failed — see individual record errors for details\"\n else:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n logger.reflect(\"Translation execution complete\", {\"run_id\": run.id, \"status\": run.status,\n \"successful\": s, \"failed\": f, \"skipped\": sk})\n return run\n\n # -- Delegation methods (thin wrappers for test-patch compatibility) --\n def _fetch_source_rows(self, job_id: str, run_id: str) -> list[dict[str, Any]]:\n return self._run_service._fetch_source_rows(job_id, run_id)\n\n def _filter_new_keys(self, job, run_id: str, source_rows: list) -> list:\n return self._run_service._filter_new_keys(job, run_id, source_rows)\n\n @staticmethod\n def _extract_chart_data_rows(response: dict[str, Any]) -> list[dict[str, Any]]:\n return RunExecutionService._extract_chart_data_rows(response)\n\n def _load_preview_edits(self, job_id: str) -> None:\n self._run_service._load_preview_edits(job_id)\n self._preview_edits_cache = self._run_service._preview_edits_cache\n\n @staticmethod\n def _compute_key_hash(source_data: dict) -> str:\n return RunExecutionService._compute_key_hash(source_data)\n\n def _update_language_stats_incremental(self, run_id: str, language_stats_map) -> None:\n self._run_service._update_language_stats_incremental(run_id, language_stats_map)\n\n def _resolve_provider_model(self, job) -> str | None:\n \"\"\"Resolve the LLM provider model name for token budget estimation.\"\"\"\n if not job.provider_id:\n return None\n try:\n p_svc = LLMProviderService(self.db)\n p = p_svc.get_provider(job.provider_id)\n if p:\n return p.default_model or \"gpt-4o-mini\"\n except Exception:\n pass\n return None\n\n def _auto_size_batches(self, job, source_rows, target_languages, provider_info=None) -> list:\n \"\"\"Split source rows into auto-sized batches based on content length.\"\"\"\n from ._batch_sizer import AdaptiveBatchSizer\n if provider_info is None:\n provider_info = self._resolve_provider_model(job)\n return AdaptiveBatchSizer(self.db, self.config_manager).auto_size_batches(\n job, source_rows, target_languages, provider_info)\n\n def _process_batch(self, job, run_id, batch_index, batch_rows, dict_snapshot_hash=None, config_hash=None) -> dict:\n from ._batch_proc import BatchProcessingService\n return BatchProcessingService(self.db, self.config_manager).process_batch(\n job, run_id, batch_index, batch_rows, dict_snapshot_hash, config_hash,\n preview_edits_cache=self._preview_edits_cache)\n\n def _insert_batch_to_target(self, job, batch_id, run_id) -> None:\n from ._batch_proc import BatchProcessingService\n BatchProcessingService(self.db, self.config_manager).insert_batch_to_target(job, batch_id, run_id)\n\n def _call_llm_for_batch(self, job, run_id, batch_rows, dict_matches, batch_id, max_tokens=8192, _recursion_depth=0) -> dict:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService(self.db).call_llm_for_batch(\n job, run_id, batch_rows, dict_matches, batch_id, max_tokens, _recursion_depth)\n\n def _call_llm(self, job, prompt, max_tokens=8192) -> tuple:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService(self.db).call_llm(job, prompt, max_tokens)\n\n @staticmethod\n def _call_openai_compatible(base_url, api_key, model, prompt, provider_type=\"openai\", max_tokens=8192, disable_reasoning=False) -> tuple:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService.call_openai_compatible(base_url, api_key, model, prompt, provider_type, max_tokens, disable_reasoning)\n\n @staticmethod\n def _parse_llm_response(response_text, expected_count, target_languages=None, finish_reason=None) -> dict:\n from ._llm_call import LLMTranslationService\n return LLMTranslationService._parse_llm_response(response_text, expected_count, target_languages, finish_reason)\n# #endregion TranslationExecutor\n" }, { "contract_id": "TranslationMetrics", "contract_type": "Module", "file_path": "backend/src/plugins/translate/metrics.py", "start_line": 1, - "end_line": 192, + "end_line": 190, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -38046,14 +38907,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS sqlalchemy, translate, metrics, job, statistics]\n# @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @PRE Database session is open.\n# @POST Metrics are aggregated and returned; no side effects.\n# @RATIONALE Live events (<90 days) + MetricSnapshot (>=90 days) fusion for complete picture.\n# @REJECTED Querying all events for all time — would be slow; MetricSnapshot provides historical summary.\n# @COMPLEXITY 4\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n MetricSnapshot,\n TranslationEvent,\n TranslationRun,\n TranslationRunLanguageStats,\n TranslationSchedule,\n)\n\n\n# #region TranslationMetrics [C:3] [TYPE Class]\n# @BRIEF Aggregate translation metrics from live events and MetricSnapshot.\nclass TranslationMetrics:\n\n def __init__(self, db: Session):\n self.db = db\n\n # region get_job_metrics [TYPE Function]\n # @PURPOSE Get aggregated metrics for a specific job.\n # @PRE job_id exists.\n # @POST Returns dict with metrics from events + latest snapshot.\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 # Per-language metrics from TranslationRunLanguageStats (live runs)\n per_language: dict[str, dict[str, int | float]] = {}\n lang_stats_rows = (\n self.db.query(TranslationRunLanguageStats)\n .join(TranslationRun, TranslationRunLanguageStats.run_id == TranslationRun.id)\n .filter(TranslationRun.job_id == job_id)\n .all()\n )\n for ls in lang_stats_rows:\n lang = ls.language_code\n if lang not in per_language:\n per_language[lang] = {\"tokens\": 0, \"cost\": 0.0, \"runs\": 0, \"translated_rows\": 0}\n per_language[lang][\"tokens\"] += ls.token_count or 0\n per_language[lang][\"cost\"] += ls.estimated_cost or 0.0\n per_language[lang][\"runs\"] += 1\n per_language[lang][\"translated_rows\"] += ls.translated_rows or 0\n\n # Merge in per-language data from MetricSnapshot (pruned period)\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 and latest_snapshot.per_language_metrics:\n for lang, snap_data in latest_snapshot.per_language_metrics.items():\n if lang not in per_language:\n per_language[lang] = {\"tokens\": 0, \"cost\": 0.0, \"runs\": 0, \"translated_rows\": 0}\n per_language[lang][\"tokens\"] += snap_data.get(\"cumulative_tokens\", 0)\n per_language[lang][\"cost\"] += snap_data.get(\"cumulative_cost\", 0.0)\n per_language[lang][\"runs\"] += snap_data.get(\"runs\", 0)\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 \"per_language_metrics\": per_language,\n }\n # endregion get_job_metrics\n\n # region get_all_metrics [TYPE Function]\n # @PURPOSE Get aggregated metrics for all jobs.\n # @POST Returns list of per-job metrics.\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" + "body": "# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS sqlalchemy, translate, metrics, job, statistics]\n# @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @RELATION DEPENDS_ON -> [TranslationSchedule]\n# @PRE Database session is open.\n# @POST Metrics are aggregated and returned; no side effects.\n# @RATIONALE Live events (<90 days) + MetricSnapshot (>=90 days) fusion for complete picture.\n# @REJECTED Querying all events for all time — would be slow; MetricSnapshot provides historical summary.\n# @COMPLEXITY 4\n\nfrom typing import Any\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n MetricSnapshot,\n TranslationRun,\n TranslationRunLanguageStats,\n TranslationSchedule,\n)\n\n\n# #region TranslationMetrics [C:3] [TYPE Class]\n# @BRIEF Aggregate translation metrics from live events and MetricSnapshot.\nclass TranslationMetrics:\n\n def __init__(self, db: Session):\n self.db = db\n\n # region get_job_metrics [TYPE Function]\n # @PURPOSE Get aggregated metrics for a specific job.\n # @PRE job_id exists.\n # @POST Returns dict with metrics from events + latest snapshot.\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 # Per-language metrics from TranslationRunLanguageStats (live runs)\n per_language: dict[str, dict[str, int | float]] = {}\n lang_stats_rows = (\n self.db.query(TranslationRunLanguageStats)\n .join(TranslationRun, TranslationRunLanguageStats.run_id == TranslationRun.id)\n .filter(TranslationRun.job_id == job_id)\n .all()\n )\n for ls in lang_stats_rows:\n lang = ls.language_code\n if lang not in per_language:\n per_language[lang] = {\"tokens\": 0, \"cost\": 0.0, \"runs\": 0, \"translated_rows\": 0}\n per_language[lang][\"tokens\"] += ls.token_count or 0\n per_language[lang][\"cost\"] += ls.estimated_cost or 0.0\n per_language[lang][\"runs\"] += 1\n per_language[lang][\"translated_rows\"] += ls.translated_rows or 0\n\n # Merge in per-language data from MetricSnapshot (pruned period)\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 and latest_snapshot.per_language_metrics:\n for lang, snap_data in latest_snapshot.per_language_metrics.items():\n if lang not in per_language:\n per_language[lang] = {\"tokens\": 0, \"cost\": 0.0, \"runs\": 0, \"translated_rows\": 0}\n per_language[lang][\"tokens\"] += snap_data.get(\"cumulative_tokens\", 0)\n per_language[lang][\"cost\"] += snap_data.get(\"cumulative_cost\", 0.0)\n per_language[lang][\"runs\"] += snap_data.get(\"runs\", 0)\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 \"per_language_metrics\": per_language,\n }\n # endregion get_job_metrics\n\n # region get_all_metrics [TYPE Function]\n # @PURPOSE Get aggregated metrics for all jobs.\n # @POST Returns list of per-job metrics.\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" }, { "contract_id": "TranslationOrchestrator", "contract_type": "Module", "file_path": "backend/src/plugins/translate/orchestrator.py", "start_line": 1, - "end_line": 176, + "end_line": 175, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -38115,7 +38976,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS sqlalchemy, tenacity, translate, orchestration, batch]\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\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .executor import TranslationExecutor\nfrom .orchestrator_aggregator import TranslationResultAggregator\nfrom .orchestrator_planner import TranslationPlanner\nfrom .orchestrator_runner import TranslationStageRunner\n\n\n# #region TranslationOrchestrator [C:5] [TYPE Class]\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# @RATIONALE Delegates to TranslationPlanner, TranslationStageRunner, TranslationResultAggregator for SOLID decomposition.\n# @REJECTED Monolithic class was rejected — violated INV_7 (single contract < 150 lines, module < 400 lines).\nclass TranslationOrchestrator:\n \"\"\"Coordinates full translation run lifecycle via delegated sub-components.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: str | None = 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._planner = TranslationPlanner(db, self.event_log, current_user)\n self._runner = TranslationStageRunner(db, config_manager, self.event_log, current_user)\n self._aggregator = TranslationResultAggregator(db, self.event_log)\n\n # region start_run [TYPE Function]\n # @PURPOSE: Start a new translation run for a job.\n # @PRE job_id exists. For manual runs, an accepted preview session must exist.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT DB writes.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n return self._planner.plan_run(\n job_id=job_id,\n is_scheduled=is_scheduled,\n trigger_type=trigger_type,\n full_translation=full_translation,\n )\n # endregion start_run\n\n # region execute_run [TYPE Function]\n # @PURPOSE: 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: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n return self._runner.execute_run(\n run=run,\n on_batch_progress=on_batch_progress,\n skip_insert=skip_insert,\n )\n # endregion execute_run\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n def retry_failed_batches(self, run_id: str) -> TranslationRun:\n return self._runner.retry_failed_batches(run_id)\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n def retry_insert(self, run_id: str) -> TranslationRun:\n return self._runner.retry_insert(run_id)\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n def cancel_run(self, run_id: str) -> TranslationRun:\n return self._runner.cancel_run(run_id)\n # endregion cancel_run\n\n # region _generate_and_insert_sql [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for SQL generation and insert.\n # @SIDE_EFFECT Delegates to SQLInsertService. May call Superset API.\n def _generate_and_insert_sql(\n self,\n job: Any,\n run: TranslationRun,\n ) -> dict[str, Any]:\n \"\"\"Backward-compatible wrapper — delegates to SQLInsertService.\"\"\"\n from .orchestrator_sql import SQLInsertService\n svc = SQLInsertService(self.db, self.config_manager, self.event_log)\n return svc.generate_and_insert_sql(job, run)\n # endregion _generate_and_insert_sql\n\n # region _update_language_stats [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for language stats update.\n # @SIDE_EFFECT Delegates to TranslationResultAggregator.update_language_stats.\n def _update_language_stats(\n self,\n run_id: str,\n language_stats_map: dict[str, Any],\n ) -> None:\n \"\"\"Backward-compatible wrapper — delegates to TranslationResultAggregator.\"\"\"\n return self._aggregator.update_language_stats(run_id, language_stats_map)\n # endregion _update_language_stats\n\n # region get_run_status [TYPE Function]\n # @PURPOSE: Get run status with statistics.\n def get_run_status(self, run_id: str) -> dict[str, Any]:\n return self._aggregator.get_run_status(run_id)\n # endregion get_run_status\n\n # region get_run_records [TYPE Function]\n # @PURPOSE: Get paginated records for a run.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n ) -> dict[str, Any]:\n return self._aggregator.get_run_records(run_id, page, page_size, status_filter)\n # endregion get_run_records\n\n # region get_run_history [TYPE Function]\n # @PURPOSE: Get run history for a job.\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 return self._aggregator.get_run_history(job_id, page, page_size)\n # endregion get_run_history\n\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" + "body": "# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS sqlalchemy, tenacity, translate, orchestration, batch]\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\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .orchestrator_aggregator import TranslationResultAggregator\nfrom .orchestrator_planner import TranslationPlanner\nfrom .orchestrator_runner import TranslationStageRunner\n\n\n# #region TranslationOrchestrator [C:5] [TYPE Class]\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# @RATIONALE Delegates to TranslationPlanner, TranslationStageRunner, TranslationResultAggregator for SOLID decomposition.\n# @REJECTED Monolithic class was rejected — violated INV_7 (single contract < 150 lines, module < 400 lines).\nclass TranslationOrchestrator:\n \"\"\"Coordinates full translation run lifecycle via delegated sub-components.\"\"\"\n\n def __init__(\n self,\n db: Session,\n config_manager: ConfigManager,\n current_user: str | None = 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._planner = TranslationPlanner(db, self.event_log, current_user)\n self._runner = TranslationStageRunner(db, config_manager, self.event_log, current_user)\n self._aggregator = TranslationResultAggregator(db, self.event_log)\n\n # region start_run [TYPE Function]\n # @PURPOSE: Start a new translation run for a job.\n # @PRE job_id exists. For manual runs, an accepted preview session must exist.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT DB writes.\n def start_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.start_run\"):\n return self._planner.plan_run(\n job_id=job_id,\n is_scheduled=is_scheduled,\n trigger_type=trigger_type,\n full_translation=full_translation,\n )\n # endregion start_run\n\n # region execute_run [TYPE Function]\n # @PURPOSE: 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: Callable[[str, int, int, int, int], None] | None = None,\n skip_insert: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationOrchestrator.execute_run\"):\n return self._runner.execute_run(\n run=run,\n on_batch_progress=on_batch_progress,\n skip_insert=skip_insert,\n )\n # endregion execute_run\n\n # region retry_failed_batches [TYPE Function]\n # @PURPOSE: Retry failed batches in a run.\n def retry_failed_batches(self, run_id: str) -> TranslationRun:\n return self._runner.retry_failed_batches(run_id)\n # endregion retry_failed_batches\n\n # region retry_insert [TYPE Function]\n # @PURPOSE: Retry the SQL insert phase for a completed run.\n def retry_insert(self, run_id: str) -> TranslationRun:\n return self._runner.retry_insert(run_id)\n # endregion retry_insert\n\n # region cancel_run [TYPE Function]\n # @PURPOSE: Cancel a running translation.\n def cancel_run(self, run_id: str) -> TranslationRun:\n return self._runner.cancel_run(run_id)\n # endregion cancel_run\n\n # region _generate_and_insert_sql [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for SQL generation and insert.\n # @SIDE_EFFECT Delegates to SQLInsertService. May call Superset API.\n def _generate_and_insert_sql(\n self,\n job: Any,\n run: TranslationRun,\n ) -> dict[str, Any]:\n \"\"\"Backward-compatible wrapper — delegates to SQLInsertService.\"\"\"\n from .orchestrator_sql import SQLInsertService\n svc = SQLInsertService(self.db, self.config_manager, self.event_log)\n return svc.generate_and_insert_sql(job, run)\n # endregion _generate_and_insert_sql\n\n # region _update_language_stats [TYPE Function] [SEMANTICS backward-compat wrapper]\n # @PURPOSE: Backward-compatible delegating wrapper for language stats update.\n # @SIDE_EFFECT Delegates to TranslationResultAggregator.update_language_stats.\n def _update_language_stats(\n self,\n run_id: str,\n language_stats_map: dict[str, Any],\n ) -> None:\n \"\"\"Backward-compatible wrapper — delegates to TranslationResultAggregator.\"\"\"\n return self._aggregator.update_language_stats(run_id, language_stats_map)\n # endregion _update_language_stats\n\n # region get_run_status [TYPE Function]\n # @PURPOSE: Get run status with statistics.\n def get_run_status(self, run_id: str) -> dict[str, Any]:\n return self._aggregator.get_run_status(run_id)\n # endregion get_run_status\n\n # region get_run_records [TYPE Function]\n # @PURPOSE: Get paginated records for a run.\n def get_run_records(\n self,\n run_id: str,\n page: int = 1,\n page_size: int = 50,\n status_filter: str | None = None,\n ) -> dict[str, Any]:\n return self._aggregator.get_run_records(run_id, page, page_size, status_filter)\n # endregion get_run_records\n\n # region get_run_history [TYPE Function]\n # @PURPOSE: Get run history for a job.\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 return self._aggregator.get_run_history(job_id, page, page_size)\n # endregion get_run_history\n\n# #endregion TranslationOrchestrator\n# #endregion TranslationOrchestrator\n" }, { "contract_id": "TranslationResultAggregator", @@ -38319,7 +39180,7 @@ "contract_type": "Function", "file_path": "backend/src/plugins/translate/orchestrator_lang_stats.py", "start_line": 1, - "end_line": 88, + "end_line": 87, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -38350,14 +39211,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region update_language_stats [C:3] [TYPE Function] [SEMANTICS translate, language, stats, aggregation]\n# @BRIEF Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n# @SIDE_EFFECT DB writes on language_stats objects.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRunLanguageStats]\n\nfrom collections import defaultdict\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger\nfrom ...models.translate import (\n TranslationLanguage,\n TranslationRecord,\n TranslationRunLanguageStats,\n)\nfrom .events import TranslationEventLog\n\n\ndef update_language_stats(\n db: Session,\n event_log: TranslationEventLog | None,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n) -> None:\n \"\"\"Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n\n Args:\n db: SQLAlchemy Session.\n event_log: Optional TranslationEventLog for logging events.\n run_id: The run ID to aggregate stats for.\n language_stats_map: Map of language_code -> TranslationRunLanguageStats to update.\n \"\"\"\n records = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == run_id)\n .all()\n )\n record_ids = [r.id for r in records]\n\n if not record_ids:\n logger.reason(\"No records for language stats aggregation\", {\"run_id\": run_id})\n return\n\n lang_entries = (\n db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids))\n .all()\n )\n\n agg: dict[str, dict[str, int]] = defaultdict(\n lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0}\n )\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_chars = sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value)\n total_tokens = max(1, total_chars // 4)\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n num_langs = len(language_stats_map)\n if num_langs > 0:\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n\n db.flush()\n\n logger.reason(\"Language stats updated\", {\n \"run_id\": run_id,\n \"languages\": list(language_stats_map.keys()),\n \"total_tokens_est\": total_tokens,\n })\n# #endregion update_language_stats\n" + "body": "# #region update_language_stats [C:3] [TYPE Function] [SEMANTICS translate, language, stats, aggregation]\n# @BRIEF Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n# @SIDE_EFFECT DB writes on language_stats objects.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRunLanguageStats]\n\nfrom collections import defaultdict\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger\nfrom ...models.translate import (\n TranslationLanguage,\n TranslationRecord,\n TranslationRunLanguageStats,\n)\nfrom .events import TranslationEventLog\n\n\ndef update_language_stats(\n db: Session,\n event_log: TranslationEventLog | None,\n run_id: str,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n) -> None:\n \"\"\"Aggregate TranslationLanguage entries by language_code and update TranslationRunLanguageStats.\n\n Args:\n db: SQLAlchemy Session.\n event_log: Optional TranslationEventLog for logging events.\n run_id: The run ID to aggregate stats for.\n language_stats_map: Map of language_code -> TranslationRunLanguageStats to update.\n \"\"\"\n records = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.run_id == run_id)\n .all()\n )\n record_ids = [r.id for r in records]\n\n if not record_ids:\n logger.reason(\"No records for language stats aggregation\", {\"run_id\": run_id})\n return\n\n lang_entries = (\n db.query(TranslationLanguage)\n .filter(TranslationLanguage.record_id.in_(record_ids))\n .all()\n )\n\n agg: dict[str, dict[str, int]] = defaultdict(\n lambda: {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0}\n )\n for le in lang_entries:\n code = le.language_code\n agg[code][\"total\"] += 1\n if le.status in (\"translated\", \"approved\", \"edited\"):\n agg[code][\"translated\"] += 1\n elif le.status == \"failed\":\n agg[code][\"failed\"] += 1\n elif le.status == \"skipped\":\n agg[code][\"skipped\"] += 1\n\n total_chars = sum(len(le.translated_value or \"\") for le in lang_entries if le.translated_value)\n total_tokens = max(1, total_chars // 4)\n cost_per_token = 0.002 / 1000\n\n for lang_code, lang_stat in language_stats_map.items():\n data = agg.get(lang_code, {\"total\": 0, \"translated\": 0, \"failed\": 0, \"skipped\": 0})\n lang_stat.total_rows = data[\"total\"]\n lang_stat.translated_rows = data[\"translated\"]\n lang_stat.failed_rows = data[\"failed\"]\n lang_stat.skipped_rows = data[\"skipped\"]\n num_langs = len(language_stats_map)\n if num_langs > 0:\n lang_stat.token_count = total_tokens // num_langs\n lang_stat.estimated_cost = round((lang_stat.token_count / 1000) * cost_per_token, 6)\n\n db.flush()\n\n logger.reason(\"Language stats updated\", {\n \"run_id\": run_id,\n \"languages\": list(language_stats_map.keys()),\n \"total_tokens_est\": total_tokens,\n })\n# #endregion update_language_stats\n" }, { "contract_id": "TranslationPlanner", "contract_type": "Class", "file_path": "backend/src/plugins/translate/orchestrator_planner.py", "start_line": 1, - "end_line": 128, + "end_line": 127, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -38397,7 +39258,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationPlanner [C:4] [TYPE Class] [SEMANTICS sqlalchemy, translate, orchestration, planning]\n# @BRIEF Handle translation run planning: validation, config hashing, dictionary snapshot.\n# @PRE Database session and config manager are available.\n# @POST TranslationRun is planned with hashes and config snapshot; events recorded.\n# @SIDE_EFFECT Creates TranslationRun; computes config and dict hashes; logs events.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RATIONALE Split from monolithic class: validation -> orchestrator_validation, hashing -> orchestrator_config.\n\nfrom datetime import UTC, datetime\nimport hashlib\nimport json\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationPreviewSession,\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .orchestrator_config import compute_config_hash, compute_dict_snapshot_hash\nfrom .orchestrator_validation import validate_job_preconditions\n\n\nclass TranslationPlanner:\n \"\"\"Plan translation runs: validate preconditions, compute hashes, create run records.\"\"\"\n\n def __init__(self, db: Session, event_log: TranslationEventLog, current_user: str | None = None):\n self.db = db\n self.event_log = event_log\n self.current_user = current_user\n\n # region plan_run [TYPE Function]\n # @PURPOSE: Validate, compute hashes, and create a new TranslationRun.\n # @PRE job_id exists. For manual runs, an accepted preview session exists.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT DB writes; event recorded.\n def plan_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationPlanner.plan_run\"):\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 validate_job_preconditions(job, self.db, is_scheduled=is_scheduled)\n\n config_hash_value = compute_config_hash(job)\n dict_hash_value = compute_dict_snapshot_hash(self.db, job_id)\n\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 \"target_column\": job.target_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": dict_hash_value,\n \"full_translation\": full_translation,\n }\n\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 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_value,\n dict_snapshot_hash=dict_hash_value,\n created_by=self.current_user,\n created_at=datetime.now(UTC),\n )\n self.db.add(run)\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_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash_value,\n \"dict_snapshot_hash\": dict_hash_value,\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 logger.reflect(\"Run created\", {\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 plan_run\n\n# #endregion TranslationPlanner\n" + "body": "# #region TranslationPlanner [C:4] [TYPE Class] [SEMANTICS sqlalchemy, translate, orchestration, planning]\n# @BRIEF Handle translation run planning: validation, config hashing, dictionary snapshot.\n# @PRE Database session and config manager are available.\n# @POST TranslationRun is planned with hashes and config snapshot; events recorded.\n# @SIDE_EFFECT Creates TranslationRun; computes config and dict hashes; logs events.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RATIONALE Split from monolithic class: validation -> orchestrator_validation, hashing -> orchestrator_config.\n\nfrom datetime import UTC, datetime\nimport hashlib\nimport json\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationRun,\n)\nfrom .events import TranslationEventLog\nfrom .orchestrator_config import compute_config_hash, compute_dict_snapshot_hash\nfrom .orchestrator_validation import validate_job_preconditions\n\n\nclass TranslationPlanner:\n \"\"\"Plan translation runs: validate preconditions, compute hashes, create run records.\"\"\"\n\n def __init__(self, db: Session, event_log: TranslationEventLog, current_user: str | None = None):\n self.db = db\n self.event_log = event_log\n self.current_user = current_user\n\n # region plan_run [TYPE Function]\n # @PURPOSE: Validate, compute hashes, and create a new TranslationRun.\n # @PRE job_id exists. For manual runs, an accepted preview session exists.\n # @POST TranslationRun is created in PENDING status with hash fields and config snapshot.\n # @SIDE_EFFECT DB writes; event recorded.\n def plan_run(\n self,\n job_id: str,\n is_scheduled: bool = False,\n trigger_type: str | None = None,\n full_translation: bool = False,\n ) -> TranslationRun:\n with belief_scope(\"TranslationPlanner.plan_run\"):\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 validate_job_preconditions(job, self.db, is_scheduled=is_scheduled)\n\n config_hash_value = compute_config_hash(job)\n dict_hash_value = compute_dict_snapshot_hash(self.db, job_id)\n\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 \"target_column\": job.target_column,\n \"context_columns\": job.context_columns,\n \"provider_id\": job.provider_id,\n \"batch_size\": job.batch_size,\n \"upsert_strategy\": job.upsert_strategy,\n \"dictionary_ids\": dict_hash_value,\n \"full_translation\": full_translation,\n }\n\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 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_value,\n dict_snapshot_hash=dict_hash_value,\n created_by=self.current_user,\n created_at=datetime.now(UTC),\n )\n self.db.add(run)\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_STARTED\",\n payload={\n \"is_scheduled\": is_scheduled,\n \"trigger_type\": run.trigger_type,\n \"config_hash\": config_hash_value,\n \"dict_snapshot_hash\": dict_hash_value,\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 logger.reflect(\"Run created\", {\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 plan_run\n\n# #endregion TranslationPlanner\n" }, { "contract_id": "orchestrator_query", @@ -38488,7 +39349,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/orchestrator_run_completion.py", "start_line": 1, - "end_line": 132, + "end_line": 131, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -38530,7 +39391,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region orchestrator_run_completion [C:3] [TYPE Module] [SEMANTICS translate, run, completion, failure]\n# @BRIEF Post-execution run completion handlers: cancelled, success-with-insert, and failure paths.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationResultAggregator]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats\n\n\n# region handle_executor_failure [TYPE Function]\n# @PURPOSE: Handle executor failure — rollback, mark run as FAILED, log event.\n# @SIDE_EFFECT DB writes; event log.\ndef handle_executor_failure(\n db,\n event_log,\n run: TranslationRun,\n job: TranslationJob,\n error: Exception,\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Handle executor failure — rollback, mark as FAILED, log event.\"\"\"\n logger.explore(\"Translation execution failed\", {\"run_id\": run.id, \"error\": str(error)})\n try:\n db.rollback()\n except Exception:\n pass\n run = db.merge(run)\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {error}\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_FAILED\",\n payload={\"error\": str(error), \"phase\": \"translation\"},\n created_by=current_user,\n )\n db.commit()\n return run\n# endregion handle_executor_failure\n\n\n# region complete_cancelled [TYPE Function]\n# @PURPOSE: Finalize a cancelled run — update language stats and log.\n# @SIDE_EFFECT DB writes; event log.\ndef complete_cancelled(\n db,\n event_log,\n aggregator,\n run: TranslationRun,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a cancelled run.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=run.job_id, run_id=run.id, event_type=\"RUN_CANCELLED\",\n payload={\"reason\": \"cancellation_flag\"}, created_by=current_user,\n )\n db.commit()\n return run\n# endregion complete_cancelled\n\n\n# region complete_success [TYPE Function]\n# @PURPOSE: Finalize a successful run — update stats, optionally insert SQL, commit.\n# @SIDE_EFFECT DB writes; event log; Superset API call if skip_insert is False.\ndef complete_success(\n db,\n event_log,\n aggregator,\n sql_service,\n run: TranslationRun,\n job: TranslationJob,\n skip_insert: bool,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a successful run with stats update and optional SQL insert.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records, \"successful\": run.successful_records,\n \"failed\": run.failed_records, \"skipped\": run.skipped_records,\n },\n created_by=current_user,\n )\n\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True}, created_by=current_user,\n )\n db.commit()\n return run\n\n insert_result = sql_service.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 if run.status != \"FAILED\":\n run.status = \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(UTC)\n db.flush()\n\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n },\n created_by=current_user,\n )\n db.commit()\n run = db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n return run\n# endregion complete_success\n# #endregion orchestrator_run_completion\n" + "body": "# #region orchestrator_run_completion [C:3] [TYPE Module] [SEMANTICS translate, run, completion, failure]\n# @BRIEF Post-execution run completion handlers: cancelled, success-with-insert, and failure paths.\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationResultAggregator]\n# @RELATION DEPENDS_ON -> [SQLInsertService]\n# @RELATION DEPENDS_ON -> [TranslationEventLog]\n\nfrom datetime import UTC, datetime\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationRun, TranslationRunLanguageStats\n\n\n# region handle_executor_failure [TYPE Function]\n# @PURPOSE: Handle executor failure — rollback, mark run as FAILED, log event.\n# @SIDE_EFFECT DB writes; event log.\ndef handle_executor_failure(\n db,\n event_log,\n run: TranslationRun,\n job: TranslationJob,\n error: Exception,\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Handle executor failure — rollback, mark as FAILED, log event.\"\"\"\n logger.explore(\"Translation execution failed\", {\"run_id\": run.id, \"error\": str(error)})\n try:\n db.rollback()\n except Exception:\n pass\n run = db.merge(run)\n run.status = \"FAILED\"\n run.error_message = f\"Translation execution failed: {error}\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_FAILED\",\n payload={\"error\": str(error), \"phase\": \"translation\"},\n created_by=current_user,\n )\n db.commit()\n return run\n# endregion handle_executor_failure\n\n\n# region complete_cancelled [TYPE Function]\n# @PURPOSE: Finalize a cancelled run — update language stats and log.\n# @SIDE_EFFECT DB writes; event log.\ndef complete_cancelled(\n db,\n event_log,\n aggregator,\n run: TranslationRun,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a cancelled run.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=run.job_id, run_id=run.id, event_type=\"RUN_CANCELLED\",\n payload={\"reason\": \"cancellation_flag\"}, created_by=current_user,\n )\n db.commit()\n return run\n# endregion complete_cancelled\n\n\n# region complete_success [TYPE Function]\n# @PURPOSE: Finalize a successful run — update stats, optionally insert SQL, commit.\n# @SIDE_EFFECT DB writes; event log; Superset API call if skip_insert is False.\ndef complete_success(\n db,\n event_log,\n aggregator,\n sql_service,\n run: TranslationRun,\n job: TranslationJob,\n skip_insert: bool,\n language_stats_map: dict[str, TranslationRunLanguageStats],\n current_user: str | None = None,\n) -> TranslationRun:\n \"\"\"Finalize a successful run with stats update and optional SQL insert.\"\"\"\n aggregator.update_language_stats(run.id, language_stats_map)\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"TRANSLATION_PHASE_COMPLETED\",\n payload={\n \"total\": run.total_records, \"successful\": run.successful_records,\n \"failed\": run.failed_records, \"skipped\": run.skipped_records,\n },\n created_by=current_user,\n )\n\n if skip_insert:\n run.status = \"COMPLETED\"\n run.completed_at = datetime.now(UTC)\n db.flush()\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\"skip_insert\": True}, created_by=current_user,\n )\n db.commit()\n return run\n\n insert_result = sql_service.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 if run.status != \"FAILED\":\n run.status = \"COMPLETED\"\n if insert_result.get(\"error_message\"):\n run.error_message = insert_result[\"error_message\"]\n run.completed_at = datetime.now(UTC)\n db.flush()\n\n event_log.log_event(\n job_id=job.id, run_id=run.id, event_type=\"RUN_COMPLETED\",\n payload={\n \"insert_status\": insert_result.get(\"status\"),\n \"query_id\": insert_result.get(\"query_id\"),\n \"total_records\": run.total_records,\n \"successful\": run.successful_records,\n \"failed\": run.failed_records,\n },\n created_by=current_user,\n )\n db.commit()\n run = db.query(TranslationRun).filter(TranslationRun.id == run.id).first()\n return run\n# endregion complete_success\n# #endregion orchestrator_run_completion\n" }, { "contract_id": "TranslationStageRunner", @@ -38624,7 +39485,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", "start_line": 1, - "end_line": 116, + "end_line": 115, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -38648,14 +39509,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region orchestrator_sql_rows [C:3] [TYPE Module] [SEMANTICS sql, insert, row, column, builders]\n# @BRIEF Column and row building utilities for SQL insertion in translate plugin.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n\nimport json\nfrom typing import Any\n\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import _normalize_timestamp_value\n\n\n# #region build_columns [C:2] [TYPE Function] [SEMANTICS sql, columns, build]\n# @BRIEF Build list of target columns for SQL INSERT from job configuration.\ndef build_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build column list for SQL INSERT from job config.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion build_columns\n\n\n# #region build_context_keys [C:1] [TYPE Function] [SEMANTICS context, keys]\n# @BRIEF Build list of context keys for SQL row data.\ndef build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context key list for SQL row data.\"\"\"\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion build_context_keys\n\n\n# #region build_rows [C:2] [TYPE Function] [SEMANTICS sql, rows, build]\n# @BRIEF Build row data for SQL INSERT with per-language expansion.\n# @SIDE_EFFECT Reads translation record language entries.\ndef build_rows(\n records: list[TranslationRecord],\n job: TranslationJob,\n effective_target: str | None,\n primary_language: str,\n context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row data for SQL INSERT with per-language expansion.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data = {key: str(source_data.get(key, \"\")) for key in context_keys}\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n return rows_for_sql\n# #endregion build_rows\n# #endregion orchestrator_sql_rows\n" + "body": "# #region orchestrator_sql_rows [C:3] [TYPE Module] [SEMANTICS sql, insert, row, column, builders]\n# @BRIEF Column and row building utilities for SQL insertion in translate plugin.\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [TranslationJob]\n\nimport json\n\nfrom ...models.translate import TranslationJob, TranslationRecord\nfrom .sql_generator import _normalize_timestamp_value\n\n\n# #region build_columns [C:2] [TYPE Function] [SEMANTICS sql, columns, build]\n# @BRIEF Build list of target columns for SQL INSERT from job configuration.\ndef build_columns(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build column list for SQL INSERT from job config.\"\"\"\n columns: list[str] = []\n if job.target_key_cols:\n columns.extend(job.target_key_cols)\n if effective_target:\n columns.append(effective_target)\n if job.target_language_column:\n columns.append(job.target_language_column)\n if job.target_source_column:\n columns.append(job.target_source_column)\n if job.target_source_language_column:\n columns.append(job.target_source_language_column)\n columns.append(\"context\")\n columns.append(\"is_original\")\n seen: set[str] = set()\n deduped: list[str] = []\n for c in columns:\n if c and c not in seen:\n deduped.append(c)\n seen.add(c)\n return deduped\n# #endregion build_columns\n\n\n# #region build_context_keys [C:1] [TYPE Function] [SEMANTICS context, keys]\n# @BRIEF Build list of context keys for SQL row data.\ndef build_context_keys(job: TranslationJob, effective_target: str | None) -> list[str]:\n \"\"\"Build context key list for SQL row data.\"\"\"\n context_keys = list(job.context_columns or [])\n if job.translation_column and job.translation_column != effective_target and job.translation_column not in context_keys:\n context_keys.append(job.translation_column)\n return context_keys\n# #endregion build_context_keys\n\n\n# #region build_rows [C:2] [TYPE Function] [SEMANTICS sql, rows, build]\n# @BRIEF Build row data for SQL INSERT with per-language expansion.\n# @SIDE_EFFECT Reads translation record language entries.\ndef build_rows(\n records: list[TranslationRecord],\n job: TranslationJob,\n effective_target: str | None,\n primary_language: str,\n context_keys: list[str],\n) -> list[dict[str, object]]:\n \"\"\"Build row data for SQL INSERT with per-language expansion.\"\"\"\n rows_for_sql: list[dict[str, object]] = []\n for rec in records:\n source_data = rec.source_data or {}\n detected_src_lang = \"und\"\n if rec.languages and len(rec.languages) > 0:\n detected_src_lang = rec.languages[0].source_language_detected or \"und\"\n\n context_data = {key: str(source_data.get(key, \"\")) for key in context_keys}\n base_row: dict[str, object] = {}\n if job.target_key_cols:\n for k in job.target_key_cols:\n raw = source_data.get(k)\n if raw is not None:\n normalized = _normalize_timestamp_value(raw)\n base_row[k] = normalized if normalized else raw\n else:\n base_row[k] = None\n if job.target_source_column:\n base_row[job.target_source_column] = rec.source_sql or \"\"\n if job.target_source_language_column:\n base_row[job.target_source_language_column] = detected_src_lang\n base_row[\"context\"] = json.dumps(context_data, ensure_ascii=False)\n\n original_row = dict(base_row)\n if effective_target:\n original_row[effective_target] = rec.source_sql or \"\"\n if job.target_language_column:\n original_row[job.target_language_column] = detected_src_lang\n original_row[\"is_original\"] = 1\n rows_for_sql.append(original_row)\n\n if rec.languages and len(rec.languages) > 0:\n for lang in rec.languages:\n if lang.language_code == detected_src_lang:\n continue\n trans_row = dict(base_row)\n trans_value = lang.final_value or lang.translated_value or \"\"\n if effective_target:\n trans_row[effective_target] = trans_value\n if job.target_language_column:\n trans_row[job.target_language_column] = lang.language_code\n trans_row[\"is_original\"] = 0\n rows_for_sql.append(trans_row)\n else:\n fallback_row = dict(base_row)\n if effective_target:\n fallback_row[effective_target] = rec.target_sql or \"\"\n if job.target_language_column:\n fallback_row[job.target_language_column] = primary_language\n fallback_row[\"is_original\"] = 0\n rows_for_sql.append(fallback_row)\n\n return rows_for_sql\n# #endregion build_rows\n# #endregion orchestrator_sql_rows\n" }, { "contract_id": "build_columns", "contract_type": "Function", "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", - "start_line": 13, - "end_line": 37, + "start_line": 12, + "end_line": 36, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -38672,8 +39533,8 @@ "contract_id": "build_context_keys", "contract_type": "Function", "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", - "start_line": 40, - "end_line": 48, + "start_line": 39, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -38690,8 +39551,8 @@ "contract_id": "build_rows", "contract_type": "Function", "file_path": "backend/src/plugins/translate/orchestrator_sql_rows.py", - "start_line": 51, - "end_line": 115, + "start_line": 50, + "end_line": 114, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -38843,6 +39704,25 @@ "tier_source": "AutoCalculated", "body": "# #region PreviewExecutor [C:4] [TYPE Class] [SEMANTICS sqlalchemy, superset, llm, preview]\n# @BRIEF Fetch sample data from Superset and call LLM provider for preview.\n# @PRE Database session, config manager, and Superset client are available.\n# @POST Sample rows fetched, LLM called.\n# @SIDE_EFFECT Fetches data from Superset; makes HTTP calls to LLM provider.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [LLMProviderService]\n# @RELATION DEPENDS_ON -> [LLMClient]\n# @RELATION DEPENDS_ON -> [preview_response_parser]\n# @RATIONALE LLM response parsing and hash utilities extracted to preview_response_parser module for INV_7 compliance.\n\nimport json\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.translate import TranslationJob\nfrom .preview_llm_client import LLMClient\nfrom .preview_resolve_provider import resolve_provider_model as _resolve_provider_model\nfrom .preview_response_parser import (\n compute_config_hash as _compute_config_hash,\n compute_dict_snapshot_hash as _compute_dict_snapshot_hash,\n extract_data_rows as _extract_data_rows,\n parse_llm_response as _parse_llm_response,\n)\n\n\nclass PreviewExecutor:\n \"\"\"Execute preview operations: fetch sample data from Superset, call LLM, parse responses.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager):\n self.db = db\n self.config_manager = config_manager\n\n # region fetch_sample_rows [TYPE Function]\n # @PURPOSE: Fetch sample rows from Superset dataset for preview.\n # @SIDE_EFFECT Calls Superset chart data endpoint.\n def fetch_sample_rows(\n self,\n job: TranslationJob,\n sample_size: int = 10,\n env_id: str | None = None,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"PreviewExecutor.fetch_sample_rows\"):\n environments = self.config_manager.get_environments()\n target_env_id = env_id or job.environment_id or job.source_dialect or \"\"\n env_config = next((e for e in environments if e.id == target_env_id), None)\n if not env_config:\n if environments:\n env_config = environments[0]\n else:\n raise ValueError(\"No Superset environments configured\")\n\n client = SupersetClient(env_config)\n dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))\n query_context = client.build_dataset_preview_query_context(\n dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail,\n template_params={}, effective_filters=[],\n )\n queries = query_context.get(\"queries\", [])\n if queries:\n queries[0][\"row_limit\"] = sample_size\n queries[0].pop(\"result_type\", None)\n queries[0].pop(\"columns\", None)\n queries[0][\"metrics\"] = []\n query_context[\"result_type\"] = \"samples\"\n form_data = query_context.get(\"form_data\", {})\n form_data.pop(\"query_mode\", None)\n\n try:\n response = client.network.request(\n method=\"POST\", endpoint=\"/api/v1/chart/data\",\n data=json.dumps(query_context),\n headers={\"Content-Type\": \"application/json\"},\n )\n except Exception as e:\n raise ValueError(f\"Failed to fetch sample data from Superset: {e}\")\n\n return _extract_data_rows(response)\n # endregion fetch_sample_rows\n\n # region call_llm [TYPE Function]\n # @PURPOSE: Call the configured LLM provider with a prompt.\n # @SIDE_EFFECT Makes HTTP call to LLM provider.\n def call_llm(self, job: TranslationJob, prompt: str, max_tokens: int = 8192) -> str:\n with belief_scope(\"PreviewExecutor.call_llm\"):\n if not job.provider_id:\n raise ValueError(\"Job has no LLM provider configured\")\n from ...services.llm_provider import LLMProviderService\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 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 disable_reasoning = getattr(job, 'disable_reasoning', False)\n\n if provider_type not in (\"openai\", \"openai_compatible\", \"openrouter\", \"kilo\", \"litellm\"):\n raise ValueError(f\"Unsupported provider type '{provider_type}' for preview\")\n\n max_attempts = 2\n for attempt in range(max_attempts):\n try:\n response_text = LLMClient.call_openai_compatible(\n base_url=provider.base_url, api_key=api_key, model=model,\n prompt=prompt, provider_type=provider_type,\n max_tokens=max_tokens * (attempt + 1),\n disable_reasoning=disable_reasoning or (attempt > 0),\n )\n break\n except ValueError as e:\n if \"empty content\" in str(e) and attempt < max_attempts - 1:\n logger.explore(\"Empty LLM response, retrying with doubled max_tokens\")\n continue\n raise\n\n return response_text\n # endregion call_llm\n\n # region delegation_helpers [TYPE Function]\n # @PURPOSE: Backward-compatible delegation to preview_response_parser module functions.\n\n @staticmethod\n def parse_llm_response(\n response_text: str, expected_count: int,\n target_languages: list[str] | None = None,\n finish_reason: str | None = None,\n ) -> dict[str, dict[str, str]]:\n return _parse_llm_response(response_text, expected_count, target_languages, finish_reason)\n\n def resolve_provider_model(self, job: TranslationJob) -> str | None:\n return _resolve_provider_model(self.db, job)\n\n @staticmethod\n def compute_config_hash(job: TranslationJob) -> str:\n return _compute_config_hash(job)\n\n def compute_dict_snapshot_hash(self, job_id: str) -> str:\n return _compute_dict_snapshot_hash(self.db, job_id)\n # endregion delegation_helpers\n\n# #endregion PreviewExecutor\n" }, + { + "contract_id": "_get_verify", + "contract_type": "Function", + "file_path": "backend/src/plugins/translate/preview_llm_client.py", + "start_line": 13, + "end_line": 27, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "BRIEF": "Resolve SSL verification path from LLM_SSL_VERIFY env var.", + "COMPLEXITY": 1, + "RATIONALE": "Используем capath=/etc/ssl/certs/ вместо cafile, потому что" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _get_verify [C:1] [TYPE Function] [SEMANTICS translate, ssl, verify]\n# @BRIEF Resolve SSL verification path from LLM_SSL_VERIFY env var.\n# @RATIONALE Используем capath=/etc/ssl/certs/ вместо cafile, потому что\n# OpenSSL 3.x не использует intermediate CA сертификаты из cafile для\n# построения цепочки (verify code 20). capath с хеш-симлинками работает\n# корректно (verify code 0).\n# @REJECTED cafile отвергнут — OpenSSL 3.x не использует intermediate CA\n# из единого bundle-файла. Только capath с хеш-симлинками даёт code 0.\n# @POST Returns path to /etc/ssl/certs/ when enabled, False when disabled.\ndef _get_verify() -> str | bool:\n raw = os.getenv(\"LLM_SSL_VERIFY\", \"true\").strip().lower()\n if raw in (\"false\", \"0\", \"no\", \"off\"):\n return False\n return \"/etc/ssl/certs/\"\n# #endregion _get_verify\n" + }, { "contract_id": "PreviewPromptBuilder", "contract_type": "Class", @@ -38886,7 +39766,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", "start_line": 1, - "end_line": 113, + "end_line": 111, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -38910,14 +39790,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region preview_prompt_helpers [C:2] [TYPE Module] [SEMANTICS token, budget, metadata, estimate]\n# @BRIEF Token estimation metadata and budget helpers for preview prompt building.\n# @RELATION DEPENDS_ON -> [estimate_token_budget]\n# @RELATION DEPENDS_ON -> [TokenEstimator]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob\nfrom ._token_budget import DEFAULT_CONTEXT_WINDOW, estimate_token_budget\nfrom .preview_token_estimator import TokenEstimator\n\n\n# #region estimate_token_budget_for_rows [C:2] [TYPE Function] [SEMANTICS token, budget, estimate, truncate]\n# @BRIEF Check token budget and optionally truncate source rows for preview.\n# @POST Returns adjusted source_rows, actual_row_count, and token_budget dict.\ndef estimate_token_budget_for_rows(\n source_rows: list[dict[str, Any]],\n target_languages: list[str],\n job: TranslationJob,\n provider_model: str | None,\n) -> dict[str, Any]:\n \"\"\"Check token budget and optionally truncate rows for preview.\"\"\"\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n adjusted_size = token_budget_dict[\"batch_size_adjusted\"]\n if adjusted_size < actual_row_count:\n logger.explore(\n f\"Reducing preview from {actual_row_count} to {adjusted_size} rows\",\n {\n \"estimated_input\": token_budget_dict[\"estimated_input_tokens\"],\n \"estimated_output\": token_budget_dict[\"estimated_output_tokens\"],\n \"context_window\": DEFAULT_CONTEXT_WINDOW,\n },\n )\n source_rows = source_rows[:adjusted_size]\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n return {\n \"source_rows\": source_rows,\n \"actual_row_count\": actual_row_count,\n \"token_budget\": token_budget_dict,\n }\n# #endregion estimate_token_budget_for_rows\n\n\n# #region compute_build_token_metadata [C:1] [TYPE Function] [SEMANTICS token, metadata, estimate]\n# @BRIEF Compute token estimation metadata for prompt builder return dict.\n# @POST Returns prompt, row_meta, target_languages, cost estimates, and cost_warning.\ndef compute_build_token_metadata(\n prompt: str,\n row_meta: list[dict[str, Any]],\n target_languages: list[str],\n num_languages: int,\n dictionary_section: str,\n actual_row_count: int,\n sample_size: int,\n) -> dict[str, Any]:\n \"\"\"Compute token estimation metadata for the prompt builder return dict.\"\"\"\n sample_prompt_tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n sample_output_tokens = TokenEstimator.estimate_output_tokens(actual_row_count, num_languages)\n sample_total_tokens = sample_prompt_tokens + sample_output_tokens\n sample_cost = TokenEstimator.estimate_cost(sample_total_tokens)\n\n total_est_rows = sample_size * 10\n total_est_tokens = (\n TokenEstimator.estimate_prompt_tokens(\n prompt.replace(str(actual_row_count), str(total_est_rows))\n )\n + TokenEstimator.estimate_output_tokens(total_est_rows, num_languages)\n )\n total_est_cost = TokenEstimator.estimate_cost(total_est_tokens)\n\n cost_warning = TokenEstimator.check_cost_warning(\n sample_size, num_languages, sample_total_tokens, sample_cost,\n )\n\n return {\n \"prompt\": prompt,\n \"row_meta\": row_meta,\n \"target_languages\": target_languages,\n \"num_languages\": num_languages,\n \"dictionary_section\": dictionary_section,\n \"sample_prompt_tokens\": sample_prompt_tokens,\n \"sample_output_tokens\": sample_output_tokens,\n \"sample_total_tokens\": sample_total_tokens,\n \"sample_cost\": sample_cost,\n \"total_est_rows\": total_est_rows,\n \"total_est_tokens\": total_est_tokens,\n \"total_est_cost\": total_est_cost,\n \"cost_warning\": cost_warning,\n \"actual_row_count\": actual_row_count,\n }\n# #endregion compute_build_token_metadata\n# #endregion preview_prompt_helpers\n" + "body": "# #region preview_prompt_helpers [C:2] [TYPE Module] [SEMANTICS token, budget, metadata, estimate]\n# @BRIEF Token estimation metadata and budget helpers for preview prompt building.\n# @RELATION DEPENDS_ON -> [estimate_token_budget]\n# @RELATION DEPENDS_ON -> [TokenEstimator]\n\nfrom typing import Any\n\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob\nfrom ._token_budget import DEFAULT_CONTEXT_WINDOW, estimate_token_budget\nfrom .preview_token_estimator import TokenEstimator\n\n\n# #region estimate_token_budget_for_rows [C:2] [TYPE Function] [SEMANTICS token, budget, estimate, truncate]\n# @BRIEF Check token budget and optionally truncate source rows for preview.\n# @POST Returns adjusted source_rows, actual_row_count, and token_budget dict.\ndef estimate_token_budget_for_rows(\n source_rows: list[dict[str, Any]],\n target_languages: list[str],\n job: TranslationJob,\n provider_model: str | None,\n) -> dict[str, Any]:\n \"\"\"Check token budget and optionally truncate rows for preview.\"\"\"\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n adjusted_size = token_budget_dict[\"batch_size_adjusted\"]\n if adjusted_size < actual_row_count:\n logger.explore(\n f\"Reducing preview from {actual_row_count} to {adjusted_size} rows\",\n {\n \"estimated_input\": token_budget_dict[\"estimated_input_tokens\"],\n \"estimated_output\": token_budget_dict[\"estimated_output_tokens\"],\n \"context_window\": DEFAULT_CONTEXT_WINDOW,\n },\n )\n source_rows = source_rows[:adjusted_size]\n actual_row_count = len(source_rows)\n token_budget_dict = estimate_token_budget(\n source_rows=source_rows,\n target_languages=target_languages,\n source_column=job.translation_column,\n context_columns=job.context_columns,\n batch_size=actual_row_count,\n provider_info=provider_model,\n )\n\n return {\n \"source_rows\": source_rows,\n \"actual_row_count\": actual_row_count,\n \"token_budget\": token_budget_dict,\n }\n# #endregion estimate_token_budget_for_rows\n\n\n# #region compute_build_token_metadata [C:1] [TYPE Function] [SEMANTICS token, metadata, estimate]\n# @BRIEF Compute token estimation metadata for prompt builder return dict.\n# @POST Returns prompt, row_meta, target_languages, cost estimates, and cost_warning.\ndef compute_build_token_metadata(\n prompt: str,\n row_meta: list[dict[str, Any]],\n target_languages: list[str],\n num_languages: int,\n dictionary_section: str,\n actual_row_count: int,\n sample_size: int,\n) -> dict[str, Any]:\n \"\"\"Compute token estimation metadata for the prompt builder return dict.\"\"\"\n sample_prompt_tokens = TokenEstimator.estimate_prompt_tokens(prompt)\n sample_output_tokens = TokenEstimator.estimate_output_tokens(actual_row_count, num_languages)\n sample_total_tokens = sample_prompt_tokens + sample_output_tokens\n sample_cost = TokenEstimator.estimate_cost(sample_total_tokens)\n\n total_est_rows = sample_size * 10\n total_est_tokens = (\n TokenEstimator.estimate_prompt_tokens(\n prompt.replace(str(actual_row_count), str(total_est_rows))\n )\n + TokenEstimator.estimate_output_tokens(total_est_rows, num_languages)\n )\n total_est_cost = TokenEstimator.estimate_cost(total_est_tokens)\n\n cost_warning = TokenEstimator.check_cost_warning(\n sample_size, num_languages, sample_total_tokens, sample_cost,\n )\n\n return {\n \"prompt\": prompt,\n \"row_meta\": row_meta,\n \"target_languages\": target_languages,\n \"num_languages\": num_languages,\n \"dictionary_section\": dictionary_section,\n \"sample_prompt_tokens\": sample_prompt_tokens,\n \"sample_output_tokens\": sample_output_tokens,\n \"sample_total_tokens\": sample_total_tokens,\n \"sample_cost\": sample_cost,\n \"total_est_rows\": total_est_rows,\n \"total_est_tokens\": total_est_tokens,\n \"total_est_cost\": total_est_cost,\n \"cost_warning\": cost_warning,\n \"actual_row_count\": actual_row_count,\n }\n# #endregion compute_build_token_metadata\n# #endregion preview_prompt_helpers\n" }, { "contract_id": "estimate_token_budget_for_rows", "contract_type": "Function", "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", - "start_line": 16, - "end_line": 62, + "start_line": 14, + "end_line": 60, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -38935,8 +39815,8 @@ "contract_id": "compute_build_token_metadata", "contract_type": "Function", "file_path": "backend/src/plugins/translate/preview_prompt_helpers.py", - "start_line": 65, - "end_line": 112, + "start_line": 63, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39060,7 +39940,7 @@ "contract_type": "Class", "file_path": "backend/src/plugins/translate/preview_review.py", "start_line": 1, - "end_line": 137, + "end_line": 136, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -39097,7 +39977,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PreviewSessionManager [C:3] [TYPE Class] [SEMANTICS preview, session, review, approve]\n# @BRIEF Manage preview session row-level actions: approve/edit/reject rows.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewLanguage]\n# @RELATION DEPENDS_ON -> [preview_session_ops]\n# @RATIONALE Session accept/get and serialization extracted to preview_session_ops module for INV_7 compliance.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import (\n TranslationJob,\n TranslationPreviewLanguage,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n)\nfrom .preview_session_ops import (\n accept_preview_session as _accept_preview_session,\n apply_language_action as _apply_language_action,\n apply_record_action as _apply_record_action,\n get_preview_session as _get_preview_session,\n)\n\n\nclass PreviewSessionManager:\n \"\"\"Manage preview session lifecycle: row actions, accept, query.\"\"\"\n\n def __init__(self, db: Session):\n self.db = db\n\n # region update_preview_row [TYPE Function]\n # @PURPOSE: Approve, edit, or reject an individual preview row (optionally per language).\n def update_preview_row(\n self,\n job_id: str,\n row_id: str,\n action: str,\n translation: str | None = None,\n feedback: str | None = None,\n language_code: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.update_preview_row\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"ACTIVE\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No active preview session for job '{job_id}'\")\n\n record = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.id == row_id,\n TranslationPreviewRecord.session_id == session.id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Preview record '{row_id}' not found in active session\")\n\n if language_code:\n lang_entry = (\n self.db.query(TranslationPreviewLanguage)\n .filter(\n TranslationPreviewLanguage.preview_record_id == row_id,\n TranslationPreviewLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(\n f\"Language entry '{language_code}' not found for record '{row_id}'\"\n )\n _apply_language_action(lang_entry, action, translation)\n\n all_langs = (\n self.db.query(TranslationPreviewLanguage)\n .filter(TranslationPreviewLanguage.preview_record_id == row_id)\n .all()\n )\n if all(le.status in (\"approved\", \"edited\") for le in all_langs):\n record.status = \"APPROVED\"\n if action == \"edit\" and translation is not None and all_langs:\n record.target_sql = all_langs[0].final_value or all_langs[0].translated_value\n else:\n _apply_record_action(record, action, translation)\n\n if feedback is not None:\n record.feedback = feedback\n\n self.db.commit()\n self.db.refresh(record)\n\n return {\n \"id\": record.id,\n \"source_sql\": record.source_sql,\n \"target_sql\": record.target_sql,\n \"status\": record.status,\n \"feedback\": record.feedback,\n \"languages\": [\n {\n \"language_code\": le.language_code,\n \"source_language_detected\": le.source_language_detected,\n \"translated_value\": le.translated_value,\n \"final_value\": le.final_value,\n \"user_edit\": le.user_edit,\n \"status\": le.status,\n \"needs_review\": le.needs_review,\n }\n for le in (record.languages or [])\n ],\n }\n # endregion update_preview_row\n\n # region accept_preview_session [TYPE Function]\n # @PURPOSE: Mark a preview session as accepted, which gates full execution.\n def accept_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.accept_preview_session\"):\n return _accept_preview_session(self.db, job_id)\n # endregion accept_preview_session\n\n # region get_preview_session [TYPE Function]\n # @PURPOSE: Get the latest preview session for a job with its records.\n def get_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.get_preview_session\"):\n return _get_preview_session(self.db, job_id)\n # endregion get_preview_session\n\n# #endregion PreviewSessionManager\n" + "body": "# #region PreviewSessionManager [C:3] [TYPE Class] [SEMANTICS preview, session, review, approve]\n# @BRIEF Manage preview session row-level actions: approve/edit/reject rows.\n# @RELATION DEPENDS_ON -> [TranslationPreviewSession]\n# @RELATION DEPENDS_ON -> [TranslationPreviewRecord]\n# @RELATION DEPENDS_ON -> [TranslationPreviewLanguage]\n# @RELATION DEPENDS_ON -> [preview_session_ops]\n# @RATIONALE Session accept/get and serialization extracted to preview_session_ops module for INV_7 compliance.\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope\nfrom ...models.translate import (\n TranslationPreviewLanguage,\n TranslationPreviewRecord,\n TranslationPreviewSession,\n)\nfrom .preview_session_ops import (\n accept_preview_session as _accept_preview_session,\n apply_language_action as _apply_language_action,\n apply_record_action as _apply_record_action,\n get_preview_session as _get_preview_session,\n)\n\n\nclass PreviewSessionManager:\n \"\"\"Manage preview session lifecycle: row actions, accept, query.\"\"\"\n\n def __init__(self, db: Session):\n self.db = db\n\n # region update_preview_row [TYPE Function]\n # @PURPOSE: Approve, edit, or reject an individual preview row (optionally per language).\n def update_preview_row(\n self,\n job_id: str,\n row_id: str,\n action: str,\n translation: str | None = None,\n feedback: str | None = None,\n language_code: str | None = None,\n ) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.update_preview_row\"):\n session = (\n self.db.query(TranslationPreviewSession)\n .filter(\n TranslationPreviewSession.job_id == job_id,\n TranslationPreviewSession.status == \"ACTIVE\",\n )\n .order_by(TranslationPreviewSession.created_at.desc())\n .first()\n )\n if not session:\n raise ValueError(f\"No active preview session for job '{job_id}'\")\n\n record = (\n self.db.query(TranslationPreviewRecord)\n .filter(\n TranslationPreviewRecord.id == row_id,\n TranslationPreviewRecord.session_id == session.id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Preview record '{row_id}' not found in active session\")\n\n if language_code:\n lang_entry = (\n self.db.query(TranslationPreviewLanguage)\n .filter(\n TranslationPreviewLanguage.preview_record_id == row_id,\n TranslationPreviewLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(\n f\"Language entry '{language_code}' not found for record '{row_id}'\"\n )\n _apply_language_action(lang_entry, action, translation)\n\n all_langs = (\n self.db.query(TranslationPreviewLanguage)\n .filter(TranslationPreviewLanguage.preview_record_id == row_id)\n .all()\n )\n if all(le.status in (\"approved\", \"edited\") for le in all_langs):\n record.status = \"APPROVED\"\n if action == \"edit\" and translation is not None and all_langs:\n record.target_sql = all_langs[0].final_value or all_langs[0].translated_value\n else:\n _apply_record_action(record, action, translation)\n\n if feedback is not None:\n record.feedback = feedback\n\n self.db.commit()\n self.db.refresh(record)\n\n return {\n \"id\": record.id,\n \"source_sql\": record.source_sql,\n \"target_sql\": record.target_sql,\n \"status\": record.status,\n \"feedback\": record.feedback,\n \"languages\": [\n {\n \"language_code\": le.language_code,\n \"source_language_detected\": le.source_language_detected,\n \"translated_value\": le.translated_value,\n \"final_value\": le.final_value,\n \"user_edit\": le.user_edit,\n \"status\": le.status,\n \"needs_review\": le.needs_review,\n }\n for le in (record.languages or [])\n ],\n }\n # endregion update_preview_row\n\n # region accept_preview_session [TYPE Function]\n # @PURPOSE: Mark a preview session as accepted, which gates full execution.\n def accept_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.accept_preview_session\"):\n return _accept_preview_session(self.db, job_id)\n # endregion accept_preview_session\n\n # region get_preview_session [TYPE Function]\n # @PURPOSE: Get the latest preview session for a job with its records.\n def get_preview_session(self, job_id: str) -> dict[str, Any]:\n with belief_scope(\"PreviewSessionManager.get_preview_session\"):\n return _get_preview_session(self.db, job_id)\n # endregion get_preview_session\n\n# #endregion PreviewSessionManager\n" }, { "contract_id": "preview_session_ops", @@ -39364,7 +40244,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/service.py", "start_line": 1, - "end_line": 276, + "end_line": 274, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -39400,7 +40280,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslateJobService [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, job, datasource, dialect]\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# @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# @REJECTED Monolithic service.py — violated INV_7. Decomposed into service_datasource, service_inline_correction, service_bulk_replace, service_utils.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationJobDictionary\nfrom ...schemas.translate import TranslateJobCreate, TranslateJobUpdate\nfrom .dictionary import _validate_bcp47\nfrom .service_datasource import fetch_datasource_metadata\nfrom .service_utils import _extract_dialect, job_to_response\n\n\n# #region TranslateJobService [TYPE Class]\n# @BRIEF Service for translation job CRUD with validation and Superset integration.\nclass TranslateJobService:\n \"\"\"CRUD service for translation jobs with Superset datasource integration.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n\n # region list_jobs [TYPE Function]\n # @PURPOSE: 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: str | None = None,\n ) -> tuple[int, list[TranslationJob]]:\n query = self.db.query(TranslationJob)\n if status_filter:\n query = query.filter(TranslationJob.status == status_filter)\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 return total, jobs\n # endregion list_jobs\n\n # region get_job [TYPE Function]\n # @PURPOSE: Get a single translation job by ID.\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 [TYPE Function]\n # @PURPOSE: Create a new translation job with column validation.\n # @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.\n def create_job(self, payload: TranslateJobCreate) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Creating job '{payload.name}'\")\n if payload.source_datasource_id and not payload.translation_column:\n raise ValueError(\"A translation column is required when a datasource is selected\")\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 dialect = payload.database_dialect\n if payload.source_datasource_id and (payload.environment_id or payload.source_dialect):\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), env_id, self.config_manager,\n )\n dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect detection failed: {e}\")\n dialect = payload.source_dialect\n\n target_languages = payload.target_languages\n if not target_languages and payload.target_language:\n target_languages = [payload.target_language]\n if target_languages:\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n for lang in target_languages:\n _validate_bcp47(lang, \"target_languages\")\n\n job = TranslationJob(\n id=str(uuid.uuid4()), name=payload.name, description=payload.description,\n source_dialect=payload.source_dialect, target_dialect=payload.target_dialect,\n database_dialect=dialect, source_datasource_id=payload.source_datasource_id,\n source_table=payload.source_table, target_schema=payload.target_schema,\n target_table=payload.target_table, source_key_cols=payload.source_key_cols or [],\n target_key_cols=payload.target_key_cols or [],\n translation_column=payload.translation_column, target_column=payload.target_column,\n target_language_column=payload.target_language_column,\n target_source_column=payload.target_source_column,\n target_source_language_column=payload.target_source_language_column,\n context_columns=payload.context_columns or [], target_languages=target_languages,\n provider_id=payload.provider_id, batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy, environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\", created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=job.id, dictionary_id=dict_id,\n ))\n\n self.db.commit()\n self.db.refresh(job)\n logger.info(f\"[TranslateJobService] Created job '{job.id}'\")\n return job\n # endregion create_job\n\n # region update_job [TYPE Function]\n # @PURPOSE: Update an existing translation job.\n # @SIDE_EFFECT Re-detects database_dialect if source_datasource_id changed.\n def update_job(self, job_id: str, payload: TranslateJobUpdate) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Updating job '{job_id}'\")\n job = self.get_job(job_id)\n update_data = payload.model_dump(exclude_unset=True)\n dict_ids = update_data.pop(\"dictionary_ids\", None)\n\n if \"target_language\" in update_data:\n tl = update_data.pop(\"target_language\")\n if tl and \"target_languages\" not in update_data:\n update_data[\"target_languages\"] = [tl]\n\n if update_data.get(\"target_languages\"):\n if not isinstance(update_data[\"target_languages\"], list):\n update_data[\"target_languages\"] = [str(update_data[\"target_languages\"])]\n for lang in update_data[\"target_languages\"]:\n _validate_bcp47(lang, \"target_languages\")\n\n for field, value in update_data.items():\n if hasattr(job, field):\n setattr(job, field, value)\n\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), env_id, self.config_manager,\n )\n job.database_dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect re-detection failed: {e}\")\n\n job.updated_at = datetime.now(UTC)\n\n if dict_ids is not None:\n self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).delete()\n for dict_id in dict_ids:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=job_id, dictionary_id=dict_id,\n ))\n\n self.db.commit()\n self.db.refresh(job)\n logger.info(f\"[TranslateJobService] Updated job '{job_id}'\")\n return job\n # endregion update_job\n\n # region delete_job [TYPE Function]\n # @PURPOSE: Delete a translation job and its associations.\n def delete_job(self, job_id: str) -> None:\n logger.info(f\"[TranslateJobService] Deleting job '{job_id}'\")\n job = self.get_job(job_id)\n self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).delete()\n self.db.delete(job)\n self.db.commit()\n logger.info(f\"[TranslateJobService] Deleted job '{job_id}'\")\n # endregion delete_job\n\n # region duplicate_job [TYPE Function]\n # @PURPOSE: Duplicate a translation job with a new name.\n def duplicate_job(self, job_id: str, new_name: str | None = None) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Duplicating job '{job_id}'\")\n source = self.get_job(job_id)\n new_job = TranslationJob(\n id=str(uuid.uuid4()), name=new_name or f\"{source.name} (Copy)\",\n description=source.description, source_dialect=source.source_dialect,\n target_dialect=source.target_dialect, database_dialect=source.database_dialect,\n source_datasource_id=source.source_datasource_id, source_table=source.source_table,\n target_schema=source.target_schema, target_table=source.target_table,\n source_key_cols=source.source_key_cols, target_key_cols=source.target_key_cols,\n translation_column=source.translation_column, target_column=source.target_column,\n target_language_column=source.target_language_column,\n target_source_column=source.target_source_column,\n target_source_language_column=source.target_source_language_column,\n context_columns=source.context_columns, target_languages=source.target_languages,\n provider_id=source.provider_id, batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy, status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(new_job)\n self.db.flush()\n\n old_dicts = self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).all()\n for assoc in old_dicts:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=new_job.id, dictionary_id=assoc.dictionary_id,\n ))\n\n self.db.commit()\n self.db.refresh(new_job)\n logger.info(f\"[TranslateJobService] Duplicated job '{job_id}' -> '{new_job.id}'\")\n return new_job\n # endregion duplicate_job\n\n # region get_job_dictionary_ids [TYPE Function]\n # @PURPOSE: Get dictionary IDs attached to a job.\n def get_job_dictionary_ids(self, job_id: str) -> list[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).all()\n return [a.dictionary_id for a in assocs]\n # endregion get_job_dictionary_ids\n\n # region fetch_available_datasources [TYPE Function]\n # @PURPOSE: List available Superset datasets for translation job creation.\n def fetch_available_datasources(self, env_id: str, search: str | None = None) -> list:\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# Re-exports for backward compatibility\nfrom .service_bulk_replace import BulkFindReplaceService # noqa: E402, F401\nfrom .service_datasource import ( # noqa: E402, F401\n detect_virtual_columns,\n fetch_datasource_metadata,\n get_datasource_columns,\n get_dialect_from_database,\n)\nfrom .service_inline_correction import InlineCorrectionService # noqa: E402, F401\nfrom .service_utils import _extract_dialect, job_to_response # noqa: E402, F401\n# #endregion TranslateJobService\n" + "body": "# #region TranslateJobService [C:4] [TYPE Module] [SEMANTICS sqlalchemy, translate, job, datasource, dialect]\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# @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# @REJECTED Monolithic service.py — violated INV_7. Decomposed into service_datasource, service_inline_correction, service_bulk_replace, service_utils.\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import logger\nfrom ...models.translate import TranslationJob, TranslationJobDictionary\nfrom ...schemas.translate import TranslateJobCreate, TranslateJobUpdate\nfrom .dictionary_validation import _validate_bcp47\nfrom .service_datasource import fetch_datasource_metadata\nfrom .service_utils import _extract_dialect\n\n\n# #region TranslateJobService [TYPE Class]\n# @BRIEF Service for translation job CRUD with validation and Superset integration.\nclass TranslateJobService:\n \"\"\"CRUD service for translation jobs with Superset datasource integration.\"\"\"\n\n def __init__(self, db: Session, config_manager: ConfigManager, current_user: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.current_user = current_user\n\n # region list_jobs [TYPE Function]\n # @PURPOSE: 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: str | None = None,\n ) -> tuple[int, list[TranslationJob]]:\n query = self.db.query(TranslationJob)\n if status_filter:\n query = query.filter(TranslationJob.status == status_filter)\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 return total, jobs\n # endregion list_jobs\n\n # region get_job [TYPE Function]\n # @PURPOSE: Get a single translation job by ID.\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 [TYPE Function]\n # @PURPOSE: Create a new translation job with column validation.\n # @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.\n def create_job(self, payload: TranslateJobCreate) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Creating job '{payload.name}'\")\n if payload.source_datasource_id and not payload.translation_column:\n raise ValueError(\"A translation column is required when a datasource is selected\")\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 dialect = payload.database_dialect\n if payload.source_datasource_id and (payload.environment_id or payload.source_dialect):\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), env_id, self.config_manager,\n )\n dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect detection failed: {e}\")\n dialect = payload.source_dialect\n\n target_languages = payload.target_languages\n if not target_languages and payload.target_language:\n target_languages = [payload.target_language]\n if target_languages:\n if not isinstance(target_languages, list):\n target_languages = [str(target_languages)]\n for lang in target_languages:\n _validate_bcp47(lang, \"target_languages\")\n\n job = TranslationJob(\n id=str(uuid.uuid4()), name=payload.name, description=payload.description,\n source_dialect=payload.source_dialect, target_dialect=payload.target_dialect,\n database_dialect=dialect, source_datasource_id=payload.source_datasource_id,\n source_table=payload.source_table, target_schema=payload.target_schema,\n target_table=payload.target_table, source_key_cols=payload.source_key_cols or [],\n target_key_cols=payload.target_key_cols or [],\n translation_column=payload.translation_column, target_column=payload.target_column,\n target_language_column=payload.target_language_column,\n target_source_column=payload.target_source_column,\n target_source_language_column=payload.target_source_language_column,\n context_columns=payload.context_columns or [], target_languages=target_languages,\n provider_id=payload.provider_id, batch_size=payload.batch_size,\n upsert_strategy=payload.upsert_strategy, environment_id=payload.environment_id,\n target_database_id=payload.target_database_id,\n status=\"DRAFT\", created_by=self.current_user,\n )\n self.db.add(job)\n self.db.flush()\n\n if payload.dictionary_ids:\n for dict_id in payload.dictionary_ids:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=job.id, dictionary_id=dict_id,\n ))\n\n self.db.commit()\n self.db.refresh(job)\n logger.info(f\"[TranslateJobService] Created job '{job.id}'\")\n return job\n # endregion create_job\n\n # region update_job [TYPE Function]\n # @PURPOSE: Update an existing translation job.\n # @SIDE_EFFECT Re-detects database_dialect if source_datasource_id changed.\n def update_job(self, job_id: str, payload: TranslateJobUpdate) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Updating job '{job_id}'\")\n job = self.get_job(job_id)\n update_data = payload.model_dump(exclude_unset=True)\n dict_ids = update_data.pop(\"dictionary_ids\", None)\n\n if \"target_language\" in update_data:\n tl = update_data.pop(\"target_language\")\n if tl and \"target_languages\" not in update_data:\n update_data[\"target_languages\"] = [tl]\n\n if update_data.get(\"target_languages\"):\n if not isinstance(update_data[\"target_languages\"], list):\n update_data[\"target_languages\"] = [str(update_data[\"target_languages\"])]\n for lang in update_data[\"target_languages\"]:\n _validate_bcp47(lang, \"target_languages\")\n\n for field, value in update_data.items():\n if hasattr(job, field):\n setattr(job, field, value)\n\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), env_id, self.config_manager,\n )\n job.database_dialect = detected_dialect\n except Exception as e:\n logger.warning(f\"[TranslateJobService] Dialect re-detection failed: {e}\")\n\n job.updated_at = datetime.now(UTC)\n\n if dict_ids is not None:\n self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).delete()\n for dict_id in dict_ids:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=job_id, dictionary_id=dict_id,\n ))\n\n self.db.commit()\n self.db.refresh(job)\n logger.info(f\"[TranslateJobService] Updated job '{job_id}'\")\n return job\n # endregion update_job\n\n # region delete_job [TYPE Function]\n # @PURPOSE: Delete a translation job and its associations.\n def delete_job(self, job_id: str) -> None:\n logger.info(f\"[TranslateJobService] Deleting job '{job_id}'\")\n job = self.get_job(job_id)\n self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).delete()\n self.db.delete(job)\n self.db.commit()\n logger.info(f\"[TranslateJobService] Deleted job '{job_id}'\")\n # endregion delete_job\n\n # region duplicate_job [TYPE Function]\n # @PURPOSE: Duplicate a translation job with a new name.\n def duplicate_job(self, job_id: str, new_name: str | None = None) -> TranslationJob:\n logger.info(f\"[TranslateJobService] Duplicating job '{job_id}'\")\n source = self.get_job(job_id)\n new_job = TranslationJob(\n id=str(uuid.uuid4()), name=new_name or f\"{source.name} (Copy)\",\n description=source.description, source_dialect=source.source_dialect,\n target_dialect=source.target_dialect, database_dialect=source.database_dialect,\n source_datasource_id=source.source_datasource_id, source_table=source.source_table,\n target_schema=source.target_schema, target_table=source.target_table,\n source_key_cols=source.source_key_cols, target_key_cols=source.target_key_cols,\n translation_column=source.translation_column, target_column=source.target_column,\n target_language_column=source.target_language_column,\n target_source_column=source.target_source_column,\n target_source_language_column=source.target_source_language_column,\n context_columns=source.context_columns, target_languages=source.target_languages,\n provider_id=source.provider_id, batch_size=source.batch_size,\n upsert_strategy=source.upsert_strategy, status=\"DRAFT\",\n created_by=self.current_user,\n )\n self.db.add(new_job)\n self.db.flush()\n\n old_dicts = self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).all()\n for assoc in old_dicts:\n self.db.add(TranslationJobDictionary(\n id=str(uuid.uuid4()), job_id=new_job.id, dictionary_id=assoc.dictionary_id,\n ))\n\n self.db.commit()\n self.db.refresh(new_job)\n logger.info(f\"[TranslateJobService] Duplicated job '{job_id}' -> '{new_job.id}'\")\n return new_job\n # endregion duplicate_job\n\n # region get_job_dictionary_ids [TYPE Function]\n # @PURPOSE: Get dictionary IDs attached to a job.\n def get_job_dictionary_ids(self, job_id: str) -> list[str]:\n assocs = self.db.query(TranslationJobDictionary).filter(TranslationJobDictionary.job_id == job_id).all()\n return [a.dictionary_id for a in assocs]\n # endregion get_job_dictionary_ids\n\n # region fetch_available_datasources [TYPE Function]\n # @PURPOSE: List available Superset datasets for translation job creation.\n def fetch_available_datasources(self, env_id: str, search: str | None = None) -> list:\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_id\": db_info.get(\"id\"), # <-- добавлено: ID БД для target_database_id\n \"database_dialect\": dialect,\n \"description\": ds.get(\"description\", \"\"),\n })\n return result\n # endregion fetch_available_datasources\n# #endregion TranslateJobService\n\n\n# Re-exports for backward compatibility\nfrom .service_bulk_replace import BulkFindReplaceService # noqa: E402, F401\nfrom .service_datasource import ( # noqa: E402, F401\n detect_virtual_columns,\n get_datasource_columns,\n get_dialect_from_database,\n)\nfrom .service_inline_correction import InlineCorrectionService # noqa: E402, F401\n# #endregion TranslateJobService\n" }, { "contract_id": "BulkFindReplaceService", @@ -39532,7 +40412,7 @@ "contract_type": "Class", "file_path": "backend/src/plugins/translate/service_inline_correction.py", "start_line": 1, - "end_line": 222, + "end_line": 221, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -39571,7 +40451,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region InlineCorrectionService [C:4] [TYPE Class] [SEMANTICS translate, correction, inline, dictionary]\n# @BRIEF Service for inline editing translated values and submitting corrections to dictionaries.\n# @PRE Database session is available.\n# @POST Inline edits applied with optional dictionary submission.\n# @SIDE_EFFECT Modifies TranslationLanguage entries; optionally creates DictionaryEntry rows.\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [_normalize_term]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TranslationLanguage, TranslationRecord\nfrom ._utils import _normalize_term\n\n\nclass InlineCorrectionService:\n \"\"\"Handle inline correction of translated values with optional dictionary submission.\"\"\"\n\n @staticmethod\n def apply_inline_edit(\n db: Session,\n run_id: str,\n record_id: str,\n language_code: str,\n final_value: str,\n submit_to_dictionary: bool = False,\n dictionary_id: str | None = None,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Apply an inline edit to a TranslationLanguage entry.\n\n Updates final_value and user_edit fields. Optionally submits to dictionary.\n Returns the updated TranslationLanguage data as a dict.\n\n Args:\n context_data_override: If provided, overrides auto-captured context data.\n usage_notes: Usage notes for the dictionary entry.\n keep_context: If False, clear context on the dictionary entry.\n \"\"\"\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.id == record_id,\n TranslationRecord.run_id == run_id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found in run '{run_id}'\")\n\n lang_entry.final_value = final_value\n lang_entry.user_edit = final_value\n if lang_entry.status in (\"pending\", \"translated\"):\n lang_entry.status = \"edited\"\n db.flush()\n\n dict_result = None\n if submit_to_dictionary and dictionary_id:\n try:\n dict_result = InlineCorrectionService.submit_correction_to_dict(\n db=db, record_id=record_id, language_code=language_code,\n dictionary_id=dictionary_id, corrected_value=final_value,\n current_user=current_user, context_data_override=context_data_override,\n usage_notes=usage_notes, keep_context=keep_context,\n )\n except Exception as e:\n dict_result = {\"error\": str(e), \"action\": \"failed\"}\n\n db.commit()\n db.refresh(lang_entry)\n\n from ...schemas.translate import TranslationLanguageResponse\n response = TranslationLanguageResponse.model_validate(lang_entry)\n result = response.model_dump()\n if dict_result:\n result[\"dictionary_result\"] = dict_result\n return result\n\n @staticmethod\n def submit_correction_to_dict(\n db: Session,\n record_id: str,\n language_code: str,\n dictionary_id: str,\n corrected_value: str,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Submit a correction from an inline edit to the dictionary.\n\n Fetches the TranslationLanguage entry, auto-captures source text and context,\n and creates/updates a DictionaryEntry.\n\n Args:\n context_data_override: If provided, overrides auto-captured context_data.\n usage_notes: Optional usage notes to store on the dictionary entry.\n keep_context: If False, sets has_context=False and clears context_data.\n \"\"\"\n with belief_scope(\"InlineCorrectionService.submit_correction_to_dict\"):\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == record_id)\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found\")\n\n source_term = record.source_sql or record.source_object_name or \"\"\n if not source_term:\n raise ValueError(\"No source text available for this record\")\n\n source_language = lang_entry.source_language_detected or \"und\"\n target_language = language_code\n\n context_data = {}\n if record.source_data:\n context_data[\"source_data\"] = record.source_data\n if record.source_object_type:\n context_data[\"source_object_type\"] = record.source_object_type\n if record.source_object_name:\n context_data[\"source_object_name\"] = record.source_object_name\n if record.source_object_id:\n context_data[\"source_object_id\"] = record.source_object_id\n\n if context_data_override is not None:\n context_data = context_data_override\n context_source = \"manual\"\n else:\n context_source = \"auto\"\n\n if not keep_context:\n context_data = None\n context_source = \"manual\"\n\n normalized = _normalize_term(source_term)\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n\n result = {\"action\": \"created\", \"entry_id\": None, \"conflict\": None, \"message\": None}\n\n if existing:\n result[\"action\"] = \"conflict_detected\"\n result[\"conflict\"] = {\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_value,\n \"action\": \"keep_existing\",\n }\n result[\"message\"] = (\n f\"Existing entry found: '{existing.target_term}' \"\n f\"for source '{source_term}' ({source_language} → {target_language})\"\n )\n logger.reason(\"Correction conflict detected\", result)\n return result\n\n entry = DictionaryEntry(\n dictionary_id=dictionary_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=corrected_value.strip(),\n source_language=source_language,\n target_language=target_language,\n context_data=context_data if context_data else None,\n context_notes=None,\n has_context=bool(context_data) and keep_context,\n context_source=context_source,\n usage_notes=usage_notes,\n origin_source_language=source_language,\n origin_run_id=record.run_id,\n origin_row_key=record.id,\n origin_user_id=current_user,\n )\n db.add(entry)\n db.flush()\n result[\"entry_id\"] = entry.id\n result[\"message\"] = (\n f\"Entry created for '{source_term}' ({source_language}) → \"\n f\"'{corrected_value}' ({target_language})\"\n )\n logger.reflect(\"Dictionary entry created from correction\", result)\n return result\n# #endregion InlineCorrectionService\n" + "body": "# #region InlineCorrectionService [C:4] [TYPE Class] [SEMANTICS translate, correction, inline, dictionary]\n# @BRIEF Service for inline editing translated values and submitting corrections to dictionaries.\n# @PRE Database session is available.\n# @POST Inline edits applied with optional dictionary submission.\n# @SIDE_EFFECT Modifies TranslationLanguage entries; optionally creates DictionaryEntry rows.\n# @RELATION DEPENDS_ON -> [TranslationLanguage]\n# @RELATION DEPENDS_ON -> [TranslationRecord]\n# @RELATION DEPENDS_ON -> [DictionaryEntry]\n# @RELATION DEPENDS_ON -> [_normalize_term]\n\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger\nfrom ...models.translate import DictionaryEntry, TranslationLanguage, TranslationRecord\nfrom ._utils import _normalize_term\n\n\nclass InlineCorrectionService:\n \"\"\"Handle inline correction of translated values with optional dictionary submission.\"\"\"\n\n @staticmethod\n def apply_inline_edit(\n db: Session,\n run_id: str,\n record_id: str,\n language_code: str,\n final_value: str,\n submit_to_dictionary: bool = False,\n dictionary_id: str | None = None,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Apply an inline edit to a TranslationLanguage entry.\n\n Updates final_value and user_edit fields. Optionally submits to dictionary.\n Returns the updated TranslationLanguage data as a dict.\n\n Args:\n context_data_override: If provided, overrides auto-captured context data.\n usage_notes: Usage notes for the dictionary entry.\n keep_context: If False, clear context on the dictionary entry.\n \"\"\"\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(\n TranslationRecord.id == record_id,\n TranslationRecord.run_id == run_id,\n )\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found in run '{run_id}'\")\n\n lang_entry.final_value = final_value\n lang_entry.user_edit = final_value\n if lang_entry.status in (\"pending\", \"translated\"):\n lang_entry.status = \"edited\"\n db.flush()\n\n dict_result = None\n if submit_to_dictionary and dictionary_id:\n try:\n dict_result = InlineCorrectionService.submit_correction_to_dict(\n db=db, record_id=record_id, language_code=language_code,\n dictionary_id=dictionary_id, corrected_value=final_value,\n current_user=current_user, context_data_override=context_data_override,\n usage_notes=usage_notes, keep_context=keep_context,\n )\n except Exception as e:\n dict_result = {\"error\": str(e), \"action\": \"failed\"}\n\n db.commit()\n db.refresh(lang_entry)\n\n from ...schemas.translate import TranslationLanguageResponse\n response = TranslationLanguageResponse.model_validate(lang_entry)\n result = response.model_dump()\n if dict_result:\n result[\"dictionary_result\"] = dict_result\n return result\n\n @staticmethod\n def submit_correction_to_dict(\n db: Session,\n record_id: str,\n language_code: str,\n dictionary_id: str,\n corrected_value: str,\n current_user: str | None = None,\n context_data_override: dict | None = None,\n usage_notes: str | None = None,\n keep_context: bool = True,\n ) -> dict:\n \"\"\"Submit a correction from an inline edit to the dictionary.\n\n Fetches the TranslationLanguage entry, auto-captures source text and context,\n and creates/updates a DictionaryEntry.\n\n Args:\n context_data_override: If provided, overrides auto-captured context_data.\n usage_notes: Optional usage notes to store on the dictionary entry.\n keep_context: If False, sets has_context=False and clears context_data.\n \"\"\"\n with belief_scope(\"InlineCorrectionService.submit_correction_to_dict\"):\n lang_entry = (\n db.query(TranslationLanguage)\n .filter(\n TranslationLanguage.record_id == record_id,\n TranslationLanguage.language_code == language_code,\n )\n .first()\n )\n if not lang_entry:\n raise ValueError(f\"Language entry '{language_code}' not found for record '{record_id}'\")\n\n record = (\n db.query(TranslationRecord)\n .filter(TranslationRecord.id == record_id)\n .first()\n )\n if not record:\n raise ValueError(f\"Translation record '{record_id}' not found\")\n\n source_term = record.source_sql or record.source_object_name or \"\"\n if not source_term:\n raise ValueError(\"No source text available for this record\")\n\n source_language = lang_entry.source_language_detected or \"und\"\n target_language = language_code\n\n context_data = {}\n if record.source_data:\n context_data[\"source_data\"] = record.source_data\n if record.source_object_type:\n context_data[\"source_object_type\"] = record.source_object_type\n if record.source_object_name:\n context_data[\"source_object_name\"] = record.source_object_name\n if record.source_object_id:\n context_data[\"source_object_id\"] = record.source_object_id\n\n if context_data_override is not None:\n context_data = context_data_override\n context_source = \"manual\"\n else:\n context_source = \"auto\"\n\n if not keep_context:\n context_data = None\n context_source = \"manual\"\n\n normalized = _normalize_term(source_term)\n\n existing = (\n db.query(DictionaryEntry)\n .filter(\n DictionaryEntry.dictionary_id == dictionary_id,\n DictionaryEntry.source_term_normalized == normalized,\n DictionaryEntry.source_language == source_language,\n DictionaryEntry.target_language == target_language,\n )\n .first()\n )\n\n result = {\"action\": \"created\", \"entry_id\": None, \"conflict\": None, \"message\": None}\n\n if existing:\n result[\"action\"] = \"conflict_detected\"\n result[\"conflict\"] = {\n \"source_term\": source_term,\n \"existing_target_term\": existing.target_term,\n \"submitted_target_term\": corrected_value,\n \"action\": \"keep_existing\",\n }\n result[\"message\"] = (\n f\"Existing entry found: '{existing.target_term}' \"\n f\"for source '{source_term}' ({source_language} → {target_language})\"\n )\n logger.reason(\"Correction conflict detected\", result)\n return result\n\n entry = DictionaryEntry(\n dictionary_id=dictionary_id,\n source_term=source_term.strip(),\n source_term_normalized=normalized,\n target_term=corrected_value.strip(),\n source_language=source_language,\n target_language=target_language,\n context_data=context_data if context_data else None,\n context_notes=None,\n has_context=bool(context_data) and keep_context,\n context_source=context_source,\n usage_notes=usage_notes,\n origin_source_language=source_language,\n origin_run_id=record.run_id,\n origin_row_key=record.id,\n origin_user_id=current_user,\n )\n db.add(entry)\n db.flush()\n result[\"entry_id\"] = entry.id\n result[\"message\"] = (\n f\"Entry created for '{source_term}' ({source_language}) → \"\n f\"'{corrected_value}' ({target_language})\"\n )\n logger.reflect(\"Dictionary entry created from correction\", result)\n return result\n# #endregion InlineCorrectionService\n" }, { "contract_id": "TargetSchemaValidation", @@ -39650,7 +40530,7 @@ "contract_type": "Function", "file_path": "backend/src/plugins/translate/service_target_schema.py", "start_line": 183, - "end_line": 324, + "end_line": 351, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -39689,14 +40569,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region validate_target_table_schema [C:4] [TYPE Function] [SEMANTICS translate, schema, validate, orchestrate]\n# @BRIEF Основная функция: проверяет схему целевой таблицы через Superset SQL Lab.\n# @PRE Superset окружение и target_database_id валидны.\n# @POST Возвращает TargetSchemaValidationResponse с diff-анализом.\n# @SIDE_EFFECT Выполняет SQL-запрос к information_schema через Superset SQL Lab API.\n# @RELATION DEPENDS_ON -> [_build_expected_columns]\n# @RELATION DEPENDS_ON -> [_extract_columns_from_rows]\n# @RELATION DEPENDS_ON -> [_parse_sqllab_result]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\ndef validate_target_table_schema(\n req: TargetSchemaValidationRequest,\n config_manager: ConfigManager,\n) -> TargetSchemaValidationResponse:\n \"\"\"\n Проверяет схему целевой таблицы:\n 1. Вычисляет ожидаемые колонки\n 2. Запрашивает актуальные через Superset SQL Lab\n 3. Сравнивает и возвращает diff\n \"\"\"\n with belief_scope(\"validate_target_table_schema\"):\n # 1. Ожидаемые колонки\n expected = _build_expected_columns(req)\n expected_names = {c.name for c in expected}\n logger.reason(\"Built expected columns\",\n extra={\"payload\": {\"count\": len(expected), \"columns\": [c.name for c in expected]}})\n\n # 2. Валидация имён (SQL injection protection)\n if req.target_table and not _TABLE_NAME_RE.match(req.target_table):\n logger.explore(\"Invalid target table name rejected\",\n extra={\"payload\": {\"table\": req.target_table},\n \"error\": \"Invalid characters in table name\"})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=f\"Invalid target table name: '{req.target_table}'. Only alphanumeric and underscore allowed.\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n if req.target_schema and not _TABLE_NAME_RE.match(req.target_schema):\n logger.explore(\"Invalid target schema name rejected\",\n extra={\"payload\": {\"schema\": req.target_schema},\n \"error\": \"Invalid characters in schema name\"})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=f\"Invalid target schema name: '{req.target_schema}'. Only alphanumeric and underscore allowed.\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n\n # 3. Запрос через Superset SQL Lab\n target_ref = f\"{req.target_schema or 'public'}.{req.target_table}\"\n logger.reason(\"Querying target table schema\",\n extra={\"payload\": {\"table\": target_ref, \"env\": req.environment_id, \"db_id\": req.target_database_id}})\n\n try:\n executor = SupersetSqlLabExecutor(config_manager, req.environment_id)\n executor.resolve_database_id(target_database_id=req.target_database_id)\n backend = executor.get_database_backend() or \"\"\n\n safe_schema = (req.target_schema or \"public\").replace(\"'\", \"''\")\n safe_table = req.target_table.replace(\"'\", \"''\")\n\n # ClickHouse использует system.columns, всё остальное — information_schema.columns\n is_clickhouse = any(kw in backend.lower() for kw in (\"clickhouse\", \"ch\"))\n if is_clickhouse:\n sql = (\n f\"SELECT name, type, default_expression AS data_default \"\n f\"FROM system.columns \"\n f\"WHERE database = '{safe_schema}' \"\n f\"AND table = '{safe_table}' \"\n f\"ORDER BY position\"\n )\n else:\n sql = (\n f\"SELECT column_name, data_type, is_nullable \"\n f\"FROM information_schema.columns \"\n f\"WHERE table_schema = '{safe_schema}' \"\n f\"AND table_name = '{safe_table}' \"\n f\"ORDER BY ordinal_position\"\n )\n\n logger.reason(\"Executing SQL Lab query\",\n extra={\"payload\": {\"sql\": sql[:200], \"backend\": backend}})\n\n result = executor.execute_and_poll(sql=sql, max_polls=15, poll_interval_seconds=1.0)\n status = result.get(\"status\", \"\")\n\n if status == \"failed\":\n error_msg = result.get(\"error_message\", \"SQL Lab query failed\")\n logger.explore(\"SQL Lab query failed\",\n extra={\"payload\": {\"table\": target_ref}, \"error\": error_msg})\n return TargetSchemaValidationResponse(\n table_exists=False, error=f\"Superset SQL Lab error: {error_msg}\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n\n # Парсим data-строки результата (реальные колонки таблицы)\n data_rows, table_exists = _parse_sqllab_result(result)\n actual_cols_raw = _extract_columns_from_rows(data_rows)\n\n actual_cols = []\n for col in actual_cols_raw:\n actual_cols.append(TargetSchemaColumnInfo(\n name=col[\"name\"],\n data_type=col.get(\"type\"),\n is_nullable=col.get(\"is_nullable\", True),\n ))\n\n actual_names = {c.name for c in actual_cols}\n\n # 4. Diff\n missing = [col for col in expected if col.name not in actual_names]\n extra = [col for col in actual_cols if col.name not in expected_names]\n\n logger.reflect(\"Schema check complete\",\n extra={\"payload\": {\n \"table\": target_ref, \"table_exists\": table_exists,\n \"expected\": len(expected), \"found\": len(actual_cols),\n \"missing\": len(missing), \"extra\": len(extra),\n \"all_present\": len(missing) == 0,\n \"actual_columns\": [c.name for c in actual_cols],\n }})\n\n return TargetSchemaValidationResponse(\n table_exists=table_exists,\n expected_columns=expected,\n actual_columns=actual_cols,\n missing_columns=missing,\n extra_columns=extra,\n all_present=len(missing) == 0,\n )\n\n except Exception as e:\n logger.explore(\"Target schema validation failed\",\n extra={\"payload\": {\"table\": target_ref, \"env\": req.environment_id},\n \"error\": str(e)})\n return TargetSchemaValidationResponse(\n table_exists=False, error=f\"Failed to validate target schema: {e}\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n# #endregion validate_target_table_schema\n" + "body": "# #region validate_target_table_schema [C:4] [TYPE Function] [SEMANTICS translate, schema, validate, orchestrate]\n# @BRIEF Основная функция: проверяет схему целевой таблицы через Superset SQL Lab.\n# @PRE Superset окружение и target_database_id валидны.\n# @POST Возвращает TargetSchemaValidationResponse с diff-анализом.\n# @SIDE_EFFECT Выполняет SQL-запрос к information_schema через Superset SQL Lab API.\n# @RELATION DEPENDS_ON -> [_build_expected_columns]\n# @RELATION DEPENDS_ON -> [_extract_columns_from_rows]\n# @RELATION DEPENDS_ON -> [_parse_sqllab_result]\n# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]\ndef validate_target_table_schema(\n req: TargetSchemaValidationRequest,\n config_manager: ConfigManager,\n) -> TargetSchemaValidationResponse:\n \"\"\"\n Проверяет схему целевой таблицы:\n 1. Вычисляет ожидаемые колонки\n 2. Запрашивает актуальные через Superset SQL Lab\n 3. Сравнивает и возвращает diff\n \"\"\"\n with belief_scope(\"validate_target_table_schema\"):\n # 1. Ожидаемые колонки\n expected = _build_expected_columns(req)\n expected_names = {c.name for c in expected}\n logger.reason(\"Built expected columns\",\n extra={\"payload\": {\"count\": len(expected), \"columns\": [c.name for c in expected]}})\n\n # 2. Валидация имён (SQL injection protection)\n if req.target_table and not _TABLE_NAME_RE.match(req.target_table):\n logger.explore(\"Invalid target table name rejected\",\n extra={\"payload\": {\"table\": req.target_table},\n \"error\": \"Invalid characters in table name\"})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=f\"Invalid target table name: '{req.target_table}'. Only alphanumeric and underscore allowed.\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n if req.target_schema and not _TABLE_NAME_RE.match(req.target_schema):\n logger.explore(\"Invalid target schema name rejected\",\n extra={\"payload\": {\"schema\": req.target_schema},\n \"error\": \"Invalid characters in schema name\"})\n return TargetSchemaValidationResponse(\n table_exists=False,\n error=f\"Invalid target schema name: '{req.target_schema}'. Only alphanumeric and underscore allowed.\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n )\n\n # 3. Запрос через Superset SQL Lab\n target_ref = f\"{req.target_schema or 'public'}.{req.target_table}\"\n logger.reason(\"Querying target table schema\",\n extra={\"payload\": {\"table\": target_ref, \"env\": req.environment_id, \"db_id\": req.target_database_id}})\n\n try:\n executor = SupersetSqlLabExecutor(config_manager, req.environment_id)\n executor.resolve_database_id(target_database_id=req.target_database_id)\n backend = executor.get_database_backend() or \"\"\n db_name = executor.get_database_name() or \"unknown\"\n\n safe_schema = (req.target_schema or \"public\").replace(\"'\", \"''\")\n safe_table = req.target_table.replace(\"'\", \"''\")\n\n # Нормализуем backend через маппинг (как в get_dialect_from_database)\n # чтобы правильно обработать \"clickhousedb\" → \"clickhouse\",\n # \"greenplum\" → \"postgresql\" и другие варианты\n _backend_normalize = {\n \"clickhouse\": \"clickhouse\",\n \"clickhousedb\": \"clickhouse\",\n \"postgresql\": \"postgresql\",\n \"greenplum\": \"postgresql\",\n \"mysql\": \"mysql\",\n \"mssql\": \"mssql\",\n \"sqlite\": \"sqlite\",\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 normalized_backend = _backend_normalize.get(backend.lower().strip(), backend.lower().strip())\n # ClickHouse использует system.columns, всё остальное — information_schema.columns\n is_clickhouse = normalized_backend == \"clickhouse\"\n if is_clickhouse:\n sql = (\n f\"SELECT name, type, default_expression AS data_default \"\n f\"FROM system.columns \"\n f\"WHERE database = '{safe_schema}' \"\n f\"AND table = '{safe_table}' \"\n f\"ORDER BY position\"\n )\n else:\n sql = (\n f\"SELECT column_name, data_type, is_nullable \"\n f\"FROM information_schema.columns \"\n f\"WHERE table_schema = '{safe_schema}' \"\n f\"AND table_name = '{safe_table}' \"\n f\"ORDER BY ordinal_position\"\n )\n\n logger.reason(\"Executing SQL Lab query\",\n extra={\"payload\": {\"sql\": sql[:200], \"backend\": backend, \"database_name\": db_name}})\n\n result = executor.execute_and_poll(sql=sql, max_polls=15, poll_interval_seconds=1.0)\n status = result.get(\"status\", \"\")\n\n if status == \"failed\":\n error_msg = result.get(\"error_message\", \"SQL Lab query failed\")\n logger.explore(\"SQL Lab query failed\",\n extra={\"payload\": {\"table\": target_ref}, \"error\": error_msg})\n return TargetSchemaValidationResponse(\n table_exists=False, error=f\"Superset SQL Lab error: {error_msg}\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n database_name=db_name, database_backend=backend,\n )\n\n # Парсим data-строки результата (реальные колонки таблицы)\n data_rows, table_exists = _parse_sqllab_result(result)\n actual_cols_raw = _extract_columns_from_rows(data_rows)\n\n actual_cols = []\n for col in actual_cols_raw:\n actual_cols.append(TargetSchemaColumnInfo(\n name=col[\"name\"],\n data_type=col.get(\"type\"),\n is_nullable=col.get(\"is_nullable\", True),\n ))\n\n actual_names = {c.name for c in actual_cols}\n\n # 4. Diff\n missing = [col for col in expected if col.name not in actual_names]\n extra = [col for col in actual_cols if col.name not in expected_names]\n\n logger.reflect(\"Schema check complete\",\n extra={\"payload\": {\n \"table\": target_ref, \"table_exists\": table_exists,\n \"expected\": len(expected), \"found\": len(actual_cols),\n \"missing\": len(missing), \"extra\": len(extra),\n \"all_present\": len(missing) == 0,\n \"actual_columns\": [c.name for c in actual_cols],\n }})\n\n return TargetSchemaValidationResponse(\n table_exists=table_exists,\n expected_columns=expected,\n actual_columns=actual_cols,\n missing_columns=missing,\n extra_columns=extra,\n all_present=len(missing) == 0,\n database_name=db_name, database_backend=backend,\n )\n\n except Exception as e:\n logger.explore(\"Target schema validation failed\",\n extra={\"payload\": {\"table\": target_ref, \"env\": req.environment_id},\n \"error\": str(e)})\n return TargetSchemaValidationResponse(\n table_exists=False, error=f\"Failed to validate target schema: {e}\",\n expected_columns=expected, actual_columns=[],\n missing_columns=expected, extra_columns=[], all_present=False,\n database_name=db_name, database_backend=backend,\n )\n# #endregion validate_target_table_schema\n" }, { "contract_id": "ServiceUtils", "contract_type": "Module", "file_path": "backend/src/plugins/translate/service_utils.py", "start_line": 1, - "end_line": 60, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39707,31 +40587,31 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ServiceUtils [C:1] [TYPE Module] [SEMANTICS utils, dialect, response]\n# @BRIEF Utility functions for the translate service layer.\n\nfrom typing import Any\n\nfrom ...models.translate import TranslationJob\nfrom ...schemas.translate import TranslateJobResponse\n\n\n# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI.\"\"\"\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 [TYPE Function]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: list[str] | None = 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 target_column=job.target_column,\n target_language_column=job.target_language_column,\n target_source_column=job.target_source_column,\n target_source_language_column=job.target_source_language_column,\n context_columns=job.context_columns or [],\n target_languages=job.target_languages,\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# #endregion ServiceUtils\n" + "body": "# #region ServiceUtils [C:1] [TYPE Module] [SEMANTICS utils, dialect, response]\n# @BRIEF Utility functions for the translate service layer.\n\n\nfrom ...models.translate import TranslationJob\nfrom ...schemas.translate import TranslateJobResponse\n\n\n# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI or engine name.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI or engine name.\n\n Handles both URI formats (e.g. 'postgresql://...', 'clickhousedb://...')\n and plain engine names (e.g. 'postgresql', 'clickhousedb') returned by Superset.\n Normalises known variants like 'clickhousedb' → 'clickhouse'.\n \"\"\"\n if not backend:\n return \"unknown\"\n try:\n # Extract scheme from URI or use plain name\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n raw = dialect.lower()\n # Normalise known Superset backend variants\n dialect_map = {\n \"clickhousedb\": \"clickhouse\",\n \"greenplum\": \"postgresql\",\n }\n return dialect_map.get(raw, raw)\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n\n\n# #region job_to_response [TYPE Function]\n# @BRIEF Convert a TranslationJob ORM model to a TranslateJobResponse schema with dictionary_ids.\ndef job_to_response(job: TranslationJob, dict_ids: list[str] | None = 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 target_column=job.target_column,\n target_language_column=job.target_language_column,\n target_source_column=job.target_source_column,\n target_source_language_column=job.target_source_language_column,\n context_columns=job.context_columns or [],\n target_languages=job.target_languages,\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# #endregion ServiceUtils\n" }, { "contract_id": "_extract_dialect", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service_utils.py", - "start_line": 10, - "end_line": 22, + "start_line": 9, + "end_line": 33, "tier": "TIER_1", "complexity": 1, "metadata": { - "BRIEF": "Extract database dialect from Superset backend URI." + "BRIEF": "Extract database dialect from Superset backend URI or engine name." }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI.\"\"\"\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" + "body": "# #region _extract_dialect [TYPE Function]\n# @BRIEF Extract database dialect from Superset backend URI or engine name.\ndef _extract_dialect(backend: str) -> str:\n \"\"\"Extract dialect name from a Superset database backend URI or engine name.\n\n Handles both URI formats (e.g. 'postgresql://...', 'clickhousedb://...')\n and plain engine names (e.g. 'postgresql', 'clickhousedb') returned by Superset.\n Normalises known variants like 'clickhousedb' → 'clickhouse'.\n \"\"\"\n if not backend:\n return \"unknown\"\n try:\n # Extract scheme from URI or use plain name\n scheme = backend.split(\"://\")[0]\n dialect = scheme.split(\"+\")[0]\n raw = dialect.lower()\n # Normalise known Superset backend variants\n dialect_map = {\n \"clickhousedb\": \"clickhouse\",\n \"greenplum\": \"postgresql\",\n }\n return dialect_map.get(raw, raw)\n except Exception:\n return \"unknown\"\n# #endregion _extract_dialect\n" }, { "contract_id": "job_to_response", "contract_type": "Function", "file_path": "backend/src/plugins/translate/service_utils.py", - "start_line": 25, - "end_line": 59, + "start_line": 36, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -39778,7 +40658,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SQLGenerator [C:3] [TYPE Module] [SEMANTICS clickhouse, translate, sql, insert, generate]\n# @BRIEF Dialect-aware safe SQL generation for INSERT/UPSERT operations.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @PRE Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.\n# @POST Returns safe SQL strings for the target dialect.\n# @SIDE_EFFECT None — pure code generation.\n# @RATIONALE Dialect-aware SQL uses ON CONFLICT for PostgreSQL; plain INSERT for ClickHouse with documented limitations.\n# @REJECTED UPDATE statements — source is append-only; UPSERT covers overwrite case.\n# @REJECTED ORM-based insert bypasses Superset's SQL Lab audit trail.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...core.logger import belief_scope, logger\n\n# PostgreSQL/Greenplum dialects that support ON CONFLICT\nPOSTGRESQL_DIALECTS = {\"postgresql\", \"redshift\", \"greenplum\"}\n# Dialects that use backtick or no quoting\nCLICKHOUSE_DIALECTS = {\"clickhouse\"}\n\n\n# #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) -> str | None:\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=UTC)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n\n\n# #region _quote_identifier [C:4] [TYPE Function]\n# @BRIEF Quote an identifier per dialect rules. PostgreSQL uses double quotes; ClickHouse uses backticks.\n# @PRE identifier is a non-empty string.\n# @POST Returns safely quoted identifier.\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\n\n# #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: str | None = 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 values (both string and numeric)\n if dialect in CLICKHOUSE_DIALECTS:\n if (isinstance(value, str) and value) or isinstance(value, (int, float)):\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\n\n# #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: str | None = 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\n\n# #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: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n dialect: str | None = 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\n\n# #region generate_upsert_sql [C:4] [TYPE Function]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE.\n# @PRE dialect is postgresql-compatible. target_table, columns, key_columns are non-empty.\n# @POST Returns UPSERT SQL string or raises ValueError.\n# @COMPLEXITY 4\n\ndef generate_upsert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n key_columns: list[str],\n rows: list[dict[str, Any]],\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)\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\n\n# #region SQLGenerator [C:3] [TYPE Class]\n# @BRIEF Generate safe, dialect-appropriate SQL INSERT/UPSERT statements.\n# @PRE Job has target_schema, target_table, key columns configured.\n# @POST Returns generated SQL string for the target dialect.\nclass SQLGenerator:\n\n # region SQLGenerator.generate [TYPE Function]\n # @PURPOSE: Generate SQL for a set of rows, detecting dialect from the job configuration.\n # @PRE dialect is a supported database dialect. columns list is non-empty. rows is non-empty.\n # @POST Returns tuple of (sql_string, statement_count).\n # @SIDE_EFFECT None — pure SQL generation.\n @staticmethod\n def generate(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = None,\n upsert_strategy: str = \"MERGE\",\n ) -> tuple[str, int]:\n \"\"\"Generate dialect-appropriate INSERT/UPSERT SQL.\n\n Args:\n dialect: Target database dialect (e.g. 'postgresql', 'clickhouse').\n target_schema: Optional schema name.\n target_table: Target table name.\n columns: List of column names to insert.\n rows: List of row dicts with column values.\n key_columns: Key columns for conflict resolution (UPSERT).\n upsert_strategy: 'MERGE' (UPSERT), 'INSERT' (plain INSERT).\n\n Returns:\n Tuple of (sql_string, row_count).\n \"\"\"\n with belief_scope(\"SQLGenerator.generate\"):\n logger.reason(\"Generating SQL\", {\n \"dialect\": dialect,\n \"schema\": target_schema,\n \"table\": target_table,\n \"columns\": len(columns),\n \"rows\": len(rows),\n \"strategy\": upsert_strategy,\n })\n\n # Validate inputs\n if not target_table:\n raise ValueError(\"target_table is required\")\n if not columns:\n raise ValueError(\"At least one column is required\")\n if not rows:\n raise ValueError(\"At least one row is required\")\n\n # Build fully qualified table reference\n table_ref = target_table\n if target_schema:\n quoted_schema = _quote_identifier(target_schema, dialect)\n quoted_table = _quote_identifier(target_table, dialect)\n table_ref = f\"{quoted_schema}.{quoted_table}\"\n else:\n table_ref = _quote_identifier(target_table, dialect)\n\n # Quote columns per dialect\n quoted_columns = [_quote_identifier(c, dialect) for c in columns]\n quoted_key_columns = (\n [_quote_identifier(k, dialect) for k in key_columns]\n if key_columns\n else []\n )\n\n # Generate SQL per dialect and strategy\n use_upsert = upsert_strategy.upper() == \"MERGE\" and key_columns\n\n if dialect in POSTGRESQL_DIALECTS or dialect not in CLICKHOUSE_DIALECTS:\n # PostgreSQL and other ANSI dialects: support UPSERT via ON CONFLICT\n if use_upsert:\n sql = generate_upsert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n key_columns=quoted_key_columns,\n rows=rows,\n )\n else:\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n elif dialect in CLICKHOUSE_DIALECTS:\n # ClickHouse: plain INSERT, no ON CONFLICT support\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n if use_upsert:\n logger.reason(\"ClickHouse UPSERT not supported; using plain INSERT\", {\n \"note\": \"ClickHouse does not support ON CONFLICT. Use ReplacingMergeTree for dedup.\",\n })\n else:\n # Fallback: plain INSERT\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n\n logger.reflect(\"SQL generated\", {\n \"dialect\": dialect,\n \"row_count\": len(rows),\n \"sql_length\": len(sql),\n })\n return sql, len(rows)\n # endregion SQLGenerator.generate\n\n # region SQLGenerator.generate_batch [TYPE Function]\n # @PURPOSE: Generate separate INSERT statements for each row (batch-safe version).\n # @PRE Same as generate().\n # @POST Returns list of (sql_string, row_index) tuples.\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = 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\n\n# #endregion SQLGenerator\n# #endregion SQLGenerator\n" + "body": "# #region SQLGenerator [C:3] [TYPE Module] [SEMANTICS clickhouse, translate, sql, insert, generate]\n# @BRIEF Dialect-aware safe SQL generation for INSERT/UPSERT operations.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [TranslationJob]\n# @RELATION DEPENDS_ON -> [TranslationRun]\n# @PRE Job has target_schema and target_table configured. Dialect is one of supported SUPPORTED_DIALECTS.\n# @POST Returns safe SQL strings for the target dialect.\n# @SIDE_EFFECT None — pure code generation.\n# @RATIONALE Dialect-aware SQL uses ON CONFLICT for PostgreSQL; plain INSERT for ClickHouse with documented limitations.\n# @REJECTED UPDATE statements — source is append-only; UPSERT covers overwrite case.\n# @REJECTED ORM-based insert bypasses Superset's SQL Lab audit trail.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ...core.logger import belief_scope, logger\n\n# PostgreSQL/Greenplum dialects that support ON CONFLICT\nPOSTGRESQL_DIALECTS = {\"postgresql\", \"redshift\", \"greenplum\"}\n# Dialects that use backtick or no quoting\nCLICKHOUSE_DIALECTS = {\"clickhouse\", \"clickhousedb\"}\n\n\n# #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) -> str | None:\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=UTC)\n return dt.strftime(\"%Y-%m-%d\")\n except (OSError, OverflowError, ValueError):\n return None\n# #endregion _normalize_timestamp_value\n\n\n# #region _quote_identifier [C:4] [TYPE Function]\n# @BRIEF Quote an identifier per dialect rules. PostgreSQL uses double quotes; ClickHouse uses backticks.\n# @PRE identifier is a non-empty string.\n# @POST Returns safely quoted identifier.\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\n\n# #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: str | None = 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 values (both string and numeric)\n if dialect in CLICKHOUSE_DIALECTS:\n if (isinstance(value, str) and value) or isinstance(value, (int, float)):\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\n\n# #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: str | None = 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\n\n# #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: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n dialect: str | None = 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\n\n# #region generate_upsert_sql [C:4] [TYPE Function]\n# @BRIEF Generate PostgreSQL dialect UPSERT SQL with ON CONFLICT DO UPDATE.\n# @PRE dialect is postgresql-compatible. target_table, columns, key_columns are non-empty.\n# @POST Returns UPSERT SQL string or raises ValueError.\n# @COMPLEXITY 4\n\ndef generate_upsert_sql(\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n key_columns: list[str],\n rows: list[dict[str, Any]],\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)\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\n\n# #region SQLGenerator [C:3] [TYPE Class]\n# @BRIEF Generate safe, dialect-appropriate SQL INSERT/UPSERT statements.\n# @PRE Job has target_schema, target_table, key columns configured.\n# @POST Returns generated SQL string for the target dialect.\nclass SQLGenerator:\n\n # region SQLGenerator.generate [TYPE Function]\n # @PURPOSE: Generate SQL for a set of rows, detecting dialect from the job configuration.\n # @PRE dialect is a supported database dialect. columns list is non-empty. rows is non-empty.\n # @POST Returns tuple of (sql_string, statement_count).\n # @SIDE_EFFECT None — pure SQL generation.\n @staticmethod\n def generate(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = None,\n upsert_strategy: str = \"MERGE\",\n ) -> tuple[str, int]:\n \"\"\"Generate dialect-appropriate INSERT/UPSERT SQL.\n\n Args:\n dialect: Target database dialect (e.g. 'postgresql', 'clickhouse').\n target_schema: Optional schema name.\n target_table: Target table name.\n columns: List of column names to insert.\n rows: List of row dicts with column values.\n key_columns: Key columns for conflict resolution (UPSERT).\n upsert_strategy: 'MERGE' (UPSERT), 'INSERT' (plain INSERT).\n\n Returns:\n Tuple of (sql_string, row_count).\n \"\"\"\n with belief_scope(\"SQLGenerator.generate\"):\n logger.reason(\"Generating SQL\", {\n \"dialect\": dialect,\n \"schema\": target_schema,\n \"table\": target_table,\n \"columns\": len(columns),\n \"rows\": len(rows),\n \"strategy\": upsert_strategy,\n })\n\n # Validate inputs\n if not target_table:\n raise ValueError(\"target_table is required\")\n if not columns:\n raise ValueError(\"At least one column is required\")\n if not rows:\n raise ValueError(\"At least one row is required\")\n\n # Build fully qualified table reference\n table_ref = target_table\n if target_schema:\n quoted_schema = _quote_identifier(target_schema, dialect)\n quoted_table = _quote_identifier(target_table, dialect)\n table_ref = f\"{quoted_schema}.{quoted_table}\"\n else:\n table_ref = _quote_identifier(target_table, dialect)\n\n # Quote columns per dialect\n quoted_columns = [_quote_identifier(c, dialect) for c in columns]\n quoted_key_columns = (\n [_quote_identifier(k, dialect) for k in key_columns]\n if key_columns\n else []\n )\n\n # Generate SQL per dialect and strategy\n use_upsert = upsert_strategy.upper() == \"MERGE\" and key_columns\n\n if dialect in POSTGRESQL_DIALECTS or dialect not in CLICKHOUSE_DIALECTS:\n # PostgreSQL and other ANSI dialects: support UPSERT via ON CONFLICT\n if use_upsert:\n sql = generate_upsert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n key_columns=quoted_key_columns,\n rows=rows,\n )\n else:\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n elif dialect in CLICKHOUSE_DIALECTS:\n # ClickHouse: plain INSERT, no ON CONFLICT support\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n if use_upsert:\n logger.reason(\"ClickHouse UPSERT not supported; using plain INSERT\", {\n \"note\": \"ClickHouse does not support ON CONFLICT. Use ReplacingMergeTree for dedup.\",\n })\n else:\n # Fallback: plain INSERT\n sql = generate_insert_sql(\n target_schema=None,\n target_table=table_ref,\n columns=quoted_columns,\n rows=rows,\n dialect=dialect,\n )\n\n logger.reflect(\"SQL generated\", {\n \"dialect\": dialect,\n \"row_count\": len(rows),\n \"sql_length\": len(sql),\n })\n return sql, len(rows)\n # endregion SQLGenerator.generate\n\n # region SQLGenerator.generate_batch [TYPE Function]\n # @PURPOSE: Generate separate INSERT statements for each row (batch-safe version).\n # @PRE Same as generate().\n # @POST Returns list of (sql_string, row_index) tuples.\n @staticmethod\n def generate_batch(\n dialect: str,\n target_schema: str | None,\n target_table: str,\n columns: list[str],\n rows: list[dict[str, Any]],\n key_columns: list[str] | None = 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\n\n# #endregion SQLGenerator\n# #endregion SQLGenerator\n" }, { "contract_id": "_normalize_timestamp_value", @@ -39910,7 +40790,7 @@ "contract_type": "Module", "file_path": "backend/src/plugins/translate/superset_executor.py", "start_line": 1, - "end_line": 420, + "end_line": 434, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -39940,7 +40820,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SupersetSqlLabExecutor [C:4] [TYPE Module] [SEMANTICS translate, superset, sqllab, execute, query]\n# @BRIEF Submit SQL to Superset SQL Lab API and poll execution status.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [ConfigManager]\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# @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\nimport json\nimport time\nfrom typing import Any\nimport uuid\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\n\n\n# #region SupersetSqlLabExecutor [C:4] [TYPE Class]\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.\n# @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/; polls GET for status.\nclass SupersetSqlLabExecutor:\n\n def __init__(self, config_manager: ConfigManager, env_id: str):\n self.config_manager = config_manager\n self.env_id = env_id\n self._client: SupersetClient | None = None\n self._database_id: int | None = None\n self._database_backend: str | None = None\n\n # region _get_client [TYPE Function]\n # @PURPOSE 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.\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 [TYPE Function]\n # @PURPOSE Resolve the target database ID from the environment and fetch its backend engine.\n # @PRE database_name or database_id should be known.\n # @POST Returns database_id integer or raises ValueError. Also sets self._database_backend.\n # @SIDE_EFFECT Fetches databases list from Superset; optionally fetches single DB for backend.\n def resolve_database_id(\n self,\n database_name: str | None = None,\n target_database_id: str | None = None,\n ) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n\n # If a specific target_database_id is provided, use it directly\n if target_database_id:\n try:\n db_id = int(target_database_id)\n # Fetch full DB info to get backend\n db_info = client.get_database(db_id)\n result = db_info.get(\"result\", db_info)\n self._database_id = result.get(\"id\", db_id)\n self._database_backend = result.get(\"backend\") or result.get(\"engine\")\n logger.reason(\"Resolved database ID by target_database_id\", {\n \"target_database_id\": target_database_id,\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n except Exception as e:\n logger.explore(\"Failed to resolve by target_database_id, falling back\", {\n \"target_database_id\": target_database_id,\n \"error\": str(e),\n })\n # Fall through to default resolution\n\n # Fetch full database list with backend info\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\", \"backend\"]}\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 self._database_backend = db.get(\"backend\")\n logger.reason(\"Resolved database ID by name\", {\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\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 self._database_backend = databases[0].get(\"backend\")\n logger.reason(\"Using default database\", {\n \"database_name\": databases[0].get(\"database_name\"),\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n # endregion resolve_database_id\n\n # region get_database_backend [TYPE Function]\n # @PURPOSE Return the cached database backend/engine string.\n # @POST Returns backend string or None if not yet resolved.\n def get_database_backend(self) -> str | None:\n return self._database_backend\n # endregion get_database_backend\n\n # region execute_sql [TYPE Function]\n # @PURPOSE 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: int | None = None,\n run_async: bool = True,\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 not db_id:\n db_id = self.resolve_database_id()\n\n logger.reason(\"Submitting SQL to Superset SQL Lab\", {\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n \"run_async\": run_async,\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"runAsync\": run_async,\n \"schema\": None,\n \"tab\": \"translation-insert\",\n \"client_id\": f\"trl-{uuid.uuid4().hex[:7]}\", # max 11 chars for Superset varchar(11)\n }\n\n # Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,\n # others use /sql_lab/execute/ (with underscore)\n candidate_endpoints = [\"/api/v1/sqllab/execute/\", \"/api/v1/sql_lab/execute/\"]\n response = None\n last_error = None\n for endpoint in candidate_endpoints:\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n logger.reason(\"SQL Lab endpoint succeeded\", extra={\n \"endpoint\": endpoint,\n })\n break\n except Exception as ep_err:\n last_error = ep_err\n logger.explore(\"SQL Lab endpoint failed, trying next\", extra={\n \"error\": str(ep_err),\n \"endpoint\": endpoint,\n })\n response = None\n\n if response is None:\n error_msg = f\"All SQL Lab endpoints failed. Last error: {last_error}\"\n logger.explore(\"SQL Lab execute failed\", extra={\"error\": error_msg})\n raise ValueError(f\"Superset SQL Lab execute failed: {error_msg}\")\n\n # Parse response — try multiple known Superset response formats\n result = response if isinstance(response, dict) else {}\n\n # Superset may return query_id at top level, nested in \"result\", as \"id\", or in \"query\" block\n query_id = (\n result.get(\"query_id\")\n or result.get(\"id\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\")\n or (result.get(\"query\") or {}).get(\"query_id\")\n or (result.get(\"query\") or {}).get(\"id\")\n )\n status = result.get(\"status\", \"unknown\")\n\n # Log full response for debugging if query_id is missing\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"raw_response_keys\": list(result.keys()),\n \"raw_response_preview\": json.dumps(result)[:2000],\n })\n logger.reason(\"SQL Lab execute response (no query_id)\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\n \"has_result\": \"result\" in result,\n \"has_query\": \"query\" in result,\n \"raw_preview\": json.dumps(result)[:2000],\n })\n\n logger.reason(\"SQL Lab execute response\", {\n \"query_id\": query_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\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 [TYPE Function]\n # @PURPOSE 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.\n # @SIDE_EFFECT Makes HTTP GET requests to Superset.\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 logger.reason(\"Polling SQL execution status\", {\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 raw = response if isinstance(response, dict) else {}\n # Superset wraps query data in a nested \"result\" field\n result = raw.get(\"result\", raw)\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 logger.reason(\"SQL execution completed\", {\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 logger.explore(\"SQL execution failed\", {\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 logger.explore(\"Polling error, retrying\", {\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 logger.explore(\"SQL execution polling timed out\", {\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 [TYPE Function]\n # @PURPOSE 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: int | None = 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 run_async=False, # Sync mode — Superset returns result directly\n )\n\n status = exec_result.get(\"status\", \"unknown\")\n\n # Sync mode: response already has the final status and data\n if status == \"success\":\n logger.reason(\"SQL execution completed (sync)\", {\n \"query_id\": exec_result.get(\"query_id\"),\n })\n return {\n \"query_id\": exec_result.get(\"query_id\"),\n \"status\": \"success\",\n \"state\": \"success\",\n \"rows_affected\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"rows\"),\n \"error_message\": None,\n \"results\": exec_result.get(\"raw_response\", {}).get(\"data\"),\n \"completed_on\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"endDttm\"),\n }\n\n # For async mode (fallback): poll for completion\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", {\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 [TYPE Function]\n # @PURPOSE Fetch the results of a completed query.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns query results if available.\n # @SIDE_EFFECT Makes HTTP GET to Superset.\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 logger.explore(\"Failed to fetch query results\", {\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" + "body": "# #region SupersetSqlLabExecutor [C:4] [TYPE Module] [SEMANTICS translate, superset, sqllab, execute, query]\n# @BRIEF Submit SQL to Superset SQL Lab API and poll execution status.\n# @LAYER Infrastructure\n# @RELATION DEPENDS_ON -> [ConfigManager]\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# @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\nimport json\nimport time\nfrom typing import Any\nimport uuid\n\nfrom ...core.config_manager import ConfigManager\nfrom ...core.logger import belief_scope, logger\nfrom ...core.superset_client import SupersetClient\n\n\n# #region SupersetSqlLabExecutor [C:4] [TYPE Class]\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.\n# @SIDE_EFFECT Makes HTTP POST to /api/v1/sqllab/execute/; polls GET for status.\nclass SupersetSqlLabExecutor:\n\n def __init__(self, config_manager: ConfigManager, env_id: str):\n self.config_manager = config_manager\n self.env_id = env_id\n self._client: SupersetClient | None = None\n self._database_id: int | None = None\n self._database_backend: str | None = None\n self._database_name: str | None = None\n\n # region _get_client [TYPE Function]\n # @PURPOSE 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.\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 [TYPE Function]\n # @PURPOSE Resolve the target database ID from the environment and fetch its backend engine.\n # @PRE database_name or database_id should be known.\n # @POST Returns database_id integer or raises ValueError. Also sets self._database_backend.\n # @SIDE_EFFECT Fetches databases list from Superset; optionally fetches single DB for backend.\n def resolve_database_id(\n self,\n database_name: str | None = None,\n target_database_id: str | None = None,\n ) -> int:\n with belief_scope(\"SupersetSqlLabExecutor.resolve_database_id\"):\n client = self._get_client()\n\n # If a specific target_database_id is provided, use it directly\n if target_database_id:\n try:\n db_id = int(target_database_id)\n # Fetch full DB info to get backend\n db_info = client.get_database(db_id)\n result = db_info.get(\"result\", db_info)\n self._database_id = result.get(\"id\", db_id)\n self._database_backend = result.get(\"backend\") or result.get(\"engine\")\n self._database_name = result.get(\"database_name\", \"\")\n logger.reason(\"Resolved database ID by target_database_id\", {\n \"target_database_id\": target_database_id,\n \"database_id\": self._database_id,\n \"database_name\": self._database_name,\n \"backend\": self._database_backend,\n \"all_keys\": list(result.keys()),\n })\n return self._database_id\n except Exception as e:\n logger.explore(\"Failed to resolve by target_database_id, falling back\", {\n \"target_database_id\": target_database_id,\n \"error\": str(e),\n })\n # Fall through to default resolution\n\n # Fetch full database list with backend info\n _, databases = client.get_databases(\n query={\"columns\": [\"id\", \"database_name\", \"backend\"]}\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 self._database_backend = db.get(\"backend\")\n self._database_name = db.get(\"database_name\", \"\")\n logger.reason(\"Resolved database ID by name\", {\n \"database_name\": database_name,\n \"database_id\": self._database_id,\n \"actual_name\": self._database_name,\n \"backend\": self._database_backend,\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 self._database_backend = databases[0].get(\"backend\")\n self._database_name = databases[0].get(\"database_name\", \"\")\n logger.reason(\"Using default database\", {\n \"database_name\": self._database_name,\n \"database_id\": self._database_id,\n \"backend\": self._database_backend,\n })\n return self._database_id\n # endregion resolve_database_id\n\n # region get_database_backend [TYPE Function]\n # @PURPOSE Return the cached database backend/engine string.\n # @POST Returns backend string or None if not yet resolved.\n def get_database_backend(self) -> str | None:\n return self._database_backend\n # endregion get_database_backend\n\n # region get_database_name [TYPE Function]\n # @PURPOSE Return the cached database name.\n # @POST Returns database name string or None if not yet resolved.\n def get_database_name(self) -> str | None:\n return self._database_name\n # endregion get_database_name\n\n # region execute_sql [TYPE Function]\n # @PURPOSE 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: int | None = None,\n run_async: bool = True,\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 not db_id:\n db_id = self.resolve_database_id()\n\n logger.reason(\"Submitting SQL to Superset SQL Lab\", {\n \"database_id\": db_id,\n \"sql_length\": len(sql),\n \"run_async\": run_async,\n })\n\n payload = {\n \"database_id\": db_id,\n \"sql\": sql,\n \"runAsync\": run_async,\n \"schema\": None,\n \"tab\": \"translation-insert\",\n \"client_id\": f\"trl-{uuid.uuid4().hex[:7]}\", # max 11 chars for Superset varchar(11)\n }\n\n # Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,\n # others use /sql_lab/execute/ (with underscore)\n candidate_endpoints = [\"/api/v1/sqllab/execute/\", \"/api/v1/sql_lab/execute/\"]\n response = None\n last_error = None\n for endpoint in candidate_endpoints:\n try:\n response = client.network.request(\n method=\"POST\",\n endpoint=endpoint,\n data=json.dumps(payload),\n headers={\"Content-Type\": \"application/json\"},\n )\n if isinstance(response, dict) and response:\n logger.reason(\"SQL Lab endpoint succeeded\", extra={\n \"endpoint\": endpoint,\n })\n break\n except Exception as ep_err:\n last_error = ep_err\n logger.explore(\"SQL Lab endpoint failed, trying next\", extra={\n \"error\": str(ep_err),\n \"endpoint\": endpoint,\n })\n response = None\n\n if response is None:\n error_msg = f\"All SQL Lab endpoints failed. Last error: {last_error}\"\n logger.explore(\"SQL Lab execute failed\", extra={\"error\": error_msg})\n raise ValueError(f\"Superset SQL Lab execute failed: {error_msg}\")\n\n # Parse response — try multiple known Superset response formats\n result = response if isinstance(response, dict) else {}\n\n # Superset may return query_id at top level, nested in \"result\", as \"id\", or in \"query\" block\n query_id = (\n result.get(\"query_id\")\n or result.get(\"id\")\n or (result.get(\"result\") or {}).get(\"query_id\")\n or (result.get(\"result\") or {}).get(\"id\")\n or (result.get(\"query\") or {}).get(\"query_id\")\n or (result.get(\"query\") or {}).get(\"id\")\n )\n status = result.get(\"status\", \"unknown\")\n\n # Log full response for debugging if query_id is missing\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"raw_response_keys\": list(result.keys()),\n \"raw_response_preview\": json.dumps(result)[:2000],\n })\n logger.reason(\"SQL Lab execute response (no query_id)\", extra={\n \"database_id\": db_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\n \"has_result\": \"result\" in result,\n \"has_query\": \"query\" in result,\n \"raw_preview\": json.dumps(result)[:2000],\n })\n\n logger.reason(\"SQL Lab execute response\", {\n \"query_id\": query_id,\n \"status\": status,\n \"response_keys\": list(result.keys()),\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 [TYPE Function]\n # @PURPOSE 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.\n # @SIDE_EFFECT Makes HTTP GET requests to Superset.\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 logger.reason(\"Polling SQL execution status\", {\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 raw = response if isinstance(response, dict) else {}\n # Superset wraps query data in a nested \"result\" field\n result = raw.get(\"result\", raw)\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 logger.reason(\"SQL execution completed\", {\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 logger.explore(\"SQL execution failed\", {\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 logger.explore(\"Polling error, retrying\", {\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 logger.explore(\"SQL execution polling timed out\", {\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 [TYPE Function]\n # @PURPOSE 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: int | None = 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 run_async=False, # Sync mode — Superset returns result directly\n )\n\n status = exec_result.get(\"status\", \"unknown\")\n\n # Sync mode: response already has the final status and data\n if status == \"success\":\n logger.reason(\"SQL execution completed (sync)\", {\n \"query_id\": exec_result.get(\"query_id\"),\n })\n return {\n \"query_id\": exec_result.get(\"query_id\"),\n \"status\": \"success\",\n \"state\": \"success\",\n \"rows_affected\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"rows\"),\n \"error_message\": None,\n \"results\": exec_result.get(\"raw_response\", {}).get(\"data\"),\n \"completed_on\": exec_result.get(\"raw_response\", {}).get(\"query\", {}).get(\"endDttm\"),\n }\n\n # For async mode (fallback): poll for completion\n query_id = exec_result.get(\"query_id\")\n if not query_id:\n logger.explore(\"No query_id from SQL Lab execute\", {\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 [TYPE Function]\n # @PURPOSE Fetch the results of a completed query.\n # @PRE query_id is a valid Superset query ID.\n # @POST Returns query results if available.\n # @SIDE_EFFECT Makes HTTP GET to Superset.\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 logger.explore(\"Failed to fetch query results\", {\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" }, { "contract_id": "SchemasPackage", @@ -40061,7 +40941,7 @@ "contract_type": "Module", "file_path": "backend/src/schemas/auth.py", "start_line": 1, - "end_line": 185, + "end_line": 181, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -40082,7 +40962,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, auth, schema, token]\n#\n# @BRIEF Pydantic schemas for authentication requests and responses.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n#\n# @INVARIANT Sensitive fields like password must not be included in response schemas.\n# @DATA_CONTRACT AuthPayload -> AuthSchema\n\nimport re\nfrom datetime import datetime\n\nfrom pydantic import BaseModel, EmailStr, Field, field_validator\n\n\n# #region Token [C:1] [TYPE Class]\n# @BRIEF Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# #endregion Token\n\n\n# #region TokenData [C:1] [TYPE Class]\n# @BRIEF Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: str | None = None\n scopes: list[str] = []\n\n\n# #endregion TokenData\n\n\n# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion PermissionSchema\n\n\n# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion RoleSchema\n\n\n# #region RoleCreate [TYPE Class]\n# @BRIEF Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: str | None = None\n permissions: list[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# #endregion RoleCreate\n\n\n# #region RoleUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n permissions: list[str] | None = None\n\n\n# #endregion RoleUpdate\n\n\n# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF 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# #endregion ADGroupMappingSchema\n\n\n# #region ADGroupMappingCreate [TYPE Class]\n# @BRIEF Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n @field_validator(\"ad_group\")\n @classmethod\n def validate_ad_group(cls, v: str) -> str:\n if not v or not v.strip():\n raise ValueError(\"AD group name must not be empty\")\n # Allow DOMAIN\\groupname or distinguishedName (CN=...,DC=...) formats\n if not re.match(r'^[A-Za-z0-9_.@()=\\\\\\\\-]+$', v):\n raise ValueError(\n \"AD group name contains invalid characters. \"\n \"Use format: DOMAIN\\\\groupname or CN=groupname,DC=domain\"\n )\n return v.strip()\n\n\n# #endregion ADGroupMappingCreate\n\n\n# #region UserBase [TYPE Class]\n# @BRIEF Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: EmailStr | None = None\n is_active: bool = True\n\n\n# #endregion UserBase\n\n\n# #region UserCreate [TYPE Class]\n# @BRIEF Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str = Field(min_length=8)\n roles: list[str] = []\n\n @field_validator(\"password\")\n @classmethod\n def validate_password_strength(cls, v: str) -> str:\n if len(v) < 8:\n raise ValueError(\"Password must be at least 8 characters\")\n if not any(c.isupper() for c in v):\n raise ValueError(\"Password must contain at least one uppercase letter\")\n if not any(c.islower() for c in v):\n raise ValueError(\"Password must contain at least one lowercase letter\")\n if not any(c.isdigit() for c in v):\n raise ValueError(\"Password must contain at least one digit\")\n return v\n\n\n# #endregion UserCreate\n\n\n# #region UserUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: EmailStr | None = None\n password: str | None = None\n is_active: bool | None = None\n roles: list[str] | None = None\n\n\n# #endregion UserUpdate\n\n\n# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion User\n\n# #endregion AuthSchemas\n" + "body": "# #region AuthSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, auth, schema, token]\n#\n# @BRIEF Pydantic schemas for authentication requests and responses.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n#\n# @INVARIANT Sensitive fields like password must not be included in response schemas.\n# @DATA_CONTRACT AuthPayload -> AuthSchema\n\nfrom datetime import datetime\nimport re\n\nfrom pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator\n\n\n# #region Token [C:1] [TYPE Class]\n# @BRIEF Represents a JWT access token response.\nclass Token(BaseModel):\n access_token: str\n token_type: str\n\n\n# #endregion Token\n\n\n# #region TokenData [C:1] [TYPE Class]\n# @BRIEF Represents the data encoded in a JWT token.\nclass TokenData(BaseModel):\n username: str | None = None\n scopes: list[str] = []\n\n\n# #endregion TokenData\n\n\n# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion PermissionSchema\n\n\n# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion RoleSchema\n\n\n# #region RoleCreate [TYPE Class]\n# @BRIEF Schema for creating a new role.\nclass RoleCreate(BaseModel):\n name: str\n description: str | None = None\n permissions: list[str] = [] # List of permission IDs or \"resource:action\" strings\n\n\n# #endregion RoleCreate\n\n\n# #region RoleUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing role.\nclass RoleUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n permissions: list[str] | None = None\n\n\n# #endregion RoleUpdate\n\n\n# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ADGroupMappingSchema\n\n\n# #region ADGroupMappingCreate [TYPE Class]\n# @BRIEF Schema for creating an AD Group mapping.\nclass ADGroupMappingCreate(BaseModel):\n ad_group: str\n role_id: str\n\n @field_validator(\"ad_group\")\n @classmethod\n def validate_ad_group(cls, v: str) -> str:\n if not v or not v.strip():\n raise ValueError(\"AD group name must not be empty\")\n # Allow DOMAIN\\groupname or distinguishedName (CN=...,DC=...) formats\n if not re.match(r'^[A-Za-z0-9_.@()=\\\\\\\\-]+$', v):\n raise ValueError(\n \"AD group name contains invalid characters. \"\n \"Use format: DOMAIN\\\\groupname or CN=groupname,DC=domain\"\n )\n return v.strip()\n\n\n# #endregion ADGroupMappingCreate\n\n\n# #region UserBase [TYPE Class]\n# @BRIEF Base schema for user data.\nclass UserBase(BaseModel):\n username: str\n email: EmailStr | None = None\n is_active: bool = True\n\n\n# #endregion UserBase\n\n\n# #region UserCreate [TYPE Class]\n# @BRIEF Schema for creating a new user.\nclass UserCreate(UserBase):\n password: str = Field(min_length=8)\n roles: list[str] = []\n\n @field_validator(\"password\")\n @classmethod\n def validate_password_strength(cls, v: str) -> str:\n if len(v) < 8:\n raise ValueError(\"Password must be at least 8 characters\")\n if not any(c.isupper() for c in v):\n raise ValueError(\"Password must contain at least one uppercase letter\")\n if not any(c.islower() for c in v):\n raise ValueError(\"Password must contain at least one lowercase letter\")\n if not any(c.isdigit() for c in v):\n raise ValueError(\"Password must contain at least one digit\")\n return v\n\n\n# #endregion UserCreate\n\n\n# #region UserUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing user.\nclass UserUpdate(BaseModel):\n email: EmailStr | None = None\n password: str | None = None\n is_active: bool | None = None\n roles: list[str] | None = None\n\n\n# #endregion UserUpdate\n\n\n# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion User\n\n# #endregion AuthSchemas\n" }, { "contract_id": "Token", @@ -40125,7 +41005,7 @@ "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", "start_line": 36, - "end_line": 47, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40136,14 +41016,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n class Config:\n from_attributes = True\n\n\n# #endregion PermissionSchema\n" + "body": "# #region PermissionSchema [C:1] [TYPE Class]\n# @BRIEF Represents a permission in API responses.\nclass PermissionSchema(BaseModel):\n id: str | None = None\n resource: str\n action: str\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion PermissionSchema\n" }, { "contract_id": "RoleSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 50, - "end_line": 62, + "start_line": 49, + "end_line": 60, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40153,14 +41033,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion RoleSchema\n" + "body": "# #region RoleSchema [TYPE Class]\n# @BRIEF Represents a role in API responses.\nclass RoleSchema(BaseModel):\n id: str\n name: str\n description: str | None = None\n permissions: list[PermissionSchema] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion RoleSchema\n" }, { "contract_id": "RoleCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 65, - "end_line": 73, + "start_line": 63, + "end_line": 71, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40176,8 +41056,8 @@ "contract_id": "RoleUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 76, - "end_line": 84, + "start_line": 74, + "end_line": 82, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40193,8 +41073,8 @@ "contract_id": "ADGroupMappingSchema", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 87, - "end_line": 98, + "start_line": 85, + "end_line": 95, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40204,14 +41084,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF 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# #endregion ADGroupMappingSchema\n" + "body": "# #region ADGroupMappingSchema [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ADGroupMappingSchema\n" }, { "contract_id": "ADGroupMappingCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 101, - "end_line": 121, + "start_line": 98, + "end_line": 118, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40227,8 +41107,8 @@ "contract_id": "UserBase", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 124, - "end_line": 132, + "start_line": 121, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40244,8 +41124,8 @@ "contract_id": "UserCreate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 135, - "end_line": 155, + "start_line": 132, + "end_line": 152, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40261,8 +41141,8 @@ "contract_id": "UserUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 158, - "end_line": 167, + "start_line": 155, + "end_line": 164, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40278,8 +41158,8 @@ "contract_id": "User", "contract_type": "Class", "file_path": "backend/src/schemas/auth.py", - "start_line": 170, - "end_line": 183, + "start_line": 167, + "end_line": 179, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40289,7 +41169,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion User\n" + "body": "# #region User [TYPE Class]\n# @BRIEF Schema for user data in API responses.\nclass User(UserBase):\n id: str\n auth_source: str\n created_at: datetime\n last_login: datetime | None = None\n roles: list[RoleSchema] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion User\n" }, { "contract_id": "DatasetReviewSchemas", @@ -40317,7 +41197,7 @@ "contract_type": "Module", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", "start_line": 1, - "end_line": 210, + "end_line": 203, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40336,14 +41216,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSchemaComposites [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, composite, schema]\n# @BRIEF Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [DatasetReviewSchemaDtos]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationStatus,\n LaunchStatus,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n SessionStatus,\n)\nfrom src.schemas.dataset_review_pkg._dtos import (\n DatasetProfileDto,\n ExecutionMappingDto,\n ImportedFilterDto,\n SemanticFieldEntryDto,\n SemanticSourceDto,\n SessionCollaboratorDto,\n TemplateVariableDto,\n ValidationFindingDto,\n)\n\n\n# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF 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# #endregion ClarificationOptionDto\n\n\n# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationAnswerDto\n\n\n# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationQuestionDto\n\n\n# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationSessionDto\n\n\n# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion CompiledPreviewDto\n\n\n# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF 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: int | None = 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: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetRunContextDto\n\n\n# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionSummary\n\n\n# #region SessionDetail [C:2] [TYPE Class]\n# @BRIEF Full session detail DTO with all nested aggregates for detail views.\n# @RELATION INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: list[SessionCollaboratorDto] = []\n profile: DatasetProfileDto | None = None\n findings: list[ValidationFindingDto] = []\n semantic_sources: list[SemanticSourceDto] = []\n semantic_fields: list[SemanticFieldEntryDto] = []\n imported_filters: list[ImportedFilterDto] = []\n template_variables: list[TemplateVariableDto] = []\n execution_mappings: list[ExecutionMappingDto] = []\n clarification_sessions: list[ClarificationSessionDto] = []\n previews: list[CompiledPreviewDto] = []\n run_contexts: list[DatasetRunContextDto] = []\n\n\n# #endregion SessionDetail\n\n\n# #endregion DatasetReviewSchemaComposites\n" + "body": "# #region DatasetReviewSchemaComposites [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, composite, schema]\n# @BRIEF Composite Pydantic DTOs for clarification, preview, run context, and session summary/detail responses.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [DatasetReviewSchemaDtos]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationStatus,\n LaunchStatus,\n PreviewStatus,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n SessionStatus,\n)\nfrom src.schemas.dataset_review_pkg._dtos import (\n DatasetProfileDto,\n ExecutionMappingDto,\n ImportedFilterDto,\n SemanticFieldEntryDto,\n SemanticSourceDto,\n SessionCollaboratorDto,\n TemplateVariableDto,\n ValidationFindingDto,\n)\n\n\n# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationOptionDto\n\n\n# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationAnswerDto\n\n\n# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationQuestionDto\n\n\n# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationSessionDto\n\n\n# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion CompiledPreviewDto\n\n\n# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF 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: int | None = 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: str | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion DatasetRunContextDto\n\n\n# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SessionSummary\n\n\n# #region SessionDetail [C:2] [TYPE Class]\n# @BRIEF Full session detail DTO with all nested aggregates for detail views.\n# @RELATION INHERITS -> [SessionSummary]\nclass SessionDetail(SessionSummary):\n collaborators: list[SessionCollaboratorDto] = []\n profile: DatasetProfileDto | None = None\n findings: list[ValidationFindingDto] = []\n semantic_sources: list[SemanticSourceDto] = []\n semantic_fields: list[SemanticFieldEntryDto] = []\n imported_filters: list[ImportedFilterDto] = []\n template_variables: list[TemplateVariableDto] = []\n execution_mappings: list[ExecutionMappingDto] = []\n clarification_sessions: list[ClarificationSessionDto] = []\n previews: list[CompiledPreviewDto] = []\n run_contexts: list[DatasetRunContextDto] = []\n\n\n# #endregion SessionDetail\n\n\n# #endregion DatasetReviewSchemaComposites\n" }, { "contract_id": "ClarificationOptionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", "start_line": 34, - "end_line": 48, + "end_line": 47, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40354,14 +41234,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF 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# #endregion ClarificationOptionDto\n" + "body": "# #region ClarificationOptionDto [C:1] [TYPE Class]\n# @BRIEF 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationOptionDto\n" }, { "contract_id": "ClarificationAnswerDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 51, - "end_line": 67, + "start_line": 50, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40372,14 +41252,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationAnswerDto\n" + "body": "# #region ClarificationAnswerDto [C:1] [TYPE Class]\n# @BRIEF Clarification answer DTO with feedback.\nclass ClarificationAnswerDto(BaseModel):\n answer_id: str\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None = None\n answered_by_user_id: str\n impact_summary: str | None = None\n user_feedback: str | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationAnswerDto\n" }, { "contract_id": "ClarificationQuestionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 70, - "end_line": 90, + "start_line": 68, + "end_line": 87, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40390,14 +41270,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationQuestionDto\n" + "body": "# #region ClarificationQuestionDto [C:2] [TYPE Class]\n# @BRIEF Clarification question DTO with nested options and answer.\nclass ClarificationQuestionDto(BaseModel):\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None = None\n priority: int\n state: QuestionState\n created_at: datetime\n updated_at: datetime\n options: list[ClarificationOptionDto] = []\n answer: ClarificationAnswerDto | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationQuestionDto\n" }, { "contract_id": "ClarificationSessionDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 93, - "end_line": 112, + "start_line": 90, + "end_line": 108, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40408,14 +41288,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion ClarificationSessionDto\n" + "body": "# #region ClarificationSessionDto [C:2] [TYPE Class]\n# @BRIEF Clarification session DTO with nested questions.\nclass ClarificationSessionDto(BaseModel):\n clarification_session_id: str\n session_id: str\n status: ClarificationStatus\n current_question_id: str | None = None\n resolved_count: int\n remaining_count: int\n summary_delta: str | None = None\n started_at: datetime\n updated_at: datetime\n completed_at: datetime | None = None\n questions: list[ClarificationQuestionDto] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ClarificationSessionDto\n" }, { "contract_id": "CompiledPreviewDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 115, - "end_line": 134, + "start_line": 111, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40426,14 +41306,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion CompiledPreviewDto\n" + "body": "# #region CompiledPreviewDto [C:1] [TYPE Class]\n# @BRIEF Compiled preview DTO with fingerprint and session version.\nclass CompiledPreviewDto(BaseModel):\n preview_id: str\n session_id: str\n session_version: int | None = None\n preview_status: PreviewStatus\n compiled_sql: str | None = None\n preview_fingerprint: str\n compiled_by: str\n error_code: str | None = None\n error_details: str | None = None\n compiled_at: datetime | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion CompiledPreviewDto\n" }, { "contract_id": "DatasetRunContextDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 137, - "end_line": 160, + "start_line": 132, + "end_line": 154, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40444,14 +41324,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF 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: int | None = 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: str | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion DatasetRunContextDto\n" + "body": "# #region DatasetRunContextDto [C:1] [TYPE Class]\n# @BRIEF 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: int | None = 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: str | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion DatasetRunContextDto\n" }, { "contract_id": "SessionSummary", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 163, - "end_line": 187, + "start_line": 157, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40462,14 +41342,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SessionSummary\n" + "body": "# #region SessionSummary [C:2] [TYPE Class]\n# @BRIEF Lightweight session summary DTO for list responses.\nclass SessionSummary(BaseModel):\n session_id: str\n user_id: str\n environment_id: str\n source_kind: str\n source_input: str\n dataset_ref: str\n dataset_id: int | None = None\n version: int = 0\n session_version: int = 0\n readiness_state: ReadinessState\n recommended_action: RecommendedAction\n status: SessionStatus\n current_phase: SessionPhase\n created_at: datetime\n updated_at: datetime\n last_activity_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SessionSummary\n" }, { "contract_id": "SessionDetail", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_composites.py", - "start_line": 190, - "end_line": 207, + "start_line": 183, + "end_line": 200, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40494,7 +41374,7 @@ "contract_type": "Module", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", "start_line": 1, - "end_line": 248, + "end_line": 239, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40513,14 +41393,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSchemaDtos [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, dto, schema]\n# @BRIEF Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\nfrom src.models.dataset_review import (\n ApprovalState,\n BusinessSummarySource,\n CandidateMatchType,\n CandidateStatus,\n ConfidenceState,\n FieldKind,\n FieldProvenance,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n MappingMethod,\n MappingStatus,\n MappingWarningLevel,\n ResolutionState,\n SemanticSourceStatus,\n SemanticSourceType,\n SessionCollaboratorRole,\n TrustLevel,\n VariableKind,\n)\n\n\n# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF 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# #endregion SessionCollaboratorDto\n\n\n# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = 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# #endregion DatasetProfileDto\n\n\n# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationFindingDto\n\n\n# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF 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: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticSourceDto\n\n\n# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticCandidateDto\n\n\n# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticFieldEntryDto\n\n\n# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ImportedFilterDto\n\n\n# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF 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: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion TemplateVariableDto\n\n\n# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ExecutionMappingDto\n\n\n# #endregion DatasetReviewSchemaDtos\n" + "body": "# #region DatasetReviewSchemaDtos [C:2] [TYPE Module] [SEMANTICS pydantic, dataset, review, dto, schema]\n# @BRIEF Pydantic DTOs for session, profile, findings, collaborators, and semantic field API payloads.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict\n\nfrom src.models.dataset_review import (\n ApprovalState,\n BusinessSummarySource,\n CandidateMatchType,\n CandidateStatus,\n ConfidenceState,\n FieldKind,\n FieldProvenance,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n MappingMethod,\n MappingStatus,\n MappingWarningLevel,\n ResolutionState,\n SemanticSourceStatus,\n SemanticSourceType,\n SessionCollaboratorRole,\n TrustLevel,\n VariableKind,\n)\n\n\n# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SessionCollaboratorDto\n\n\n# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion DatasetProfileDto\n\n\n# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ValidationFindingDto\n\n\n# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF 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: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticSourceDto\n\n\n# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticCandidateDto\n\n\n# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticFieldEntryDto\n\n\n# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ImportedFilterDto\n\n\n# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF 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: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion TemplateVariableDto\n\n\n# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ExecutionMappingDto\n\n\n# #endregion DatasetReviewSchemaDtos\n" }, { "contract_id": "SessionCollaboratorDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", "start_line": 36, - "end_line": 47, + "end_line": 46, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40531,14 +41411,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF 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# #endregion SessionCollaboratorDto\n" + "body": "# #region SessionCollaboratorDto [C:1] [TYPE Class]\n# @BRIEF Collaborator DTO for session access control.\nclass SessionCollaboratorDto(BaseModel):\n user_id: str\n role: SessionCollaboratorRole\n added_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SessionCollaboratorDto\n" }, { "contract_id": "DatasetProfileDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 50, - "end_line": 75, + "start_line": 49, + "end_line": 73, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40549,14 +41429,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = 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# #endregion DatasetProfileDto\n" + "body": "# #region DatasetProfileDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n database_name: str | None = None\n business_summary: str\n business_summary_source: BusinessSummarySource\n description: str | None = None\n dataset_type: str | None = None\n is_sqllab_view: bool\n completeness_score: float | None = 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 model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion DatasetProfileDto\n" }, { "contract_id": "ValidationFindingDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 78, - "end_line": 98, + "start_line": 76, + "end_line": 95, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40567,14 +41447,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion ValidationFindingDto\n" + "body": "# #region ValidationFindingDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n caused_by_ref: str | None = None\n created_at: datetime\n resolved_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ValidationFindingDto\n" }, { "contract_id": "SemanticSourceDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 101, - "end_line": 119, + "start_line": 98, + "end_line": 115, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40585,14 +41465,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF 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: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticSourceDto\n" + "body": "# #region SemanticSourceDto [C:1] [TYPE Class]\n# @BRIEF 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: float | None = None\n status: SemanticSourceStatus\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticSourceDto\n" }, { "contract_id": "SemanticCandidateDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 122, - "end_line": 141, + "start_line": 118, + "end_line": 136, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40603,14 +41483,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticCandidateDto\n" + "body": "# #region SemanticCandidateDto [C:1] [TYPE Class]\n# @BRIEF Semantic candidate DTO with match type and confidence score.\nclass SemanticCandidateDto(BaseModel):\n candidate_id: str\n field_id: str\n source_id: str | None = None\n candidate_rank: int\n match_type: CandidateMatchType\n confidence_score: float\n proposed_verbose_name: str | None = None\n proposed_description: str | None = None\n proposed_display_format: str | None = None\n status: CandidateStatus\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticCandidateDto\n" }, { "contract_id": "SemanticFieldEntryDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 144, - "end_line": 172, + "start_line": 139, + "end_line": 166, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -40621,14 +41501,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n class Config:\n from_attributes = True\n\n\n# #endregion SemanticFieldEntryDto\n" + "body": "# #region SemanticFieldEntryDto [C:2] [TYPE Class]\n# @BRIEF Semantic field entry DTO with nested candidates and session version.\nclass SemanticFieldEntryDto(BaseModel):\n field_id: str\n session_id: str\n session_version: int | None = None\n field_name: str\n field_kind: FieldKind\n verbose_name: str | None = None\n description: str | None = None\n display_format: str | None = None\n provenance: FieldProvenance\n source_id: str | None = None\n source_version: str | None = None\n confidence_rank: int | None = None\n is_locked: bool\n has_conflict: bool\n needs_review: bool\n last_changed_by: str\n user_feedback: str | None = None\n created_at: datetime\n updated_at: datetime\n candidates: list[SemanticCandidateDto] = []\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion SemanticFieldEntryDto\n" }, { "contract_id": "ImportedFilterDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 175, - "end_line": 197, + "start_line": 169, + "end_line": 190, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40639,14 +41519,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ImportedFilterDto\n" + "body": "# #region ImportedFilterDto [C:1] [TYPE Class]\n# @BRIEF 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: str | None = None\n raw_value: Any\n raw_value_masked: bool = False\n normalized_value: Any | None = None\n source: FilterSource\n confidence_state: FilterConfidenceState\n requires_confirmation: bool\n recovery_status: FilterRecoveryStatus\n notes: str | None = None\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ImportedFilterDto\n" }, { "contract_id": "TemplateVariableDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 200, - "end_line": 218, + "start_line": 193, + "end_line": 210, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40657,14 +41537,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF 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: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion TemplateVariableDto\n" + "body": "# #region TemplateVariableDto [C:1] [TYPE Class]\n# @BRIEF 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: Any | None = None\n mapping_status: MappingStatus\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion TemplateVariableDto\n" }, { "contract_id": "ExecutionMappingDto", "contract_type": "Class", "file_path": "backend/src/schemas/dataset_review_pkg/_dtos.py", - "start_line": 221, - "end_line": 245, + "start_line": 213, + "end_line": 236, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -40675,7 +41555,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ExecutionMappingDto\n" + "body": "# #region ExecutionMappingDto [C:1] [TYPE Class]\n# @BRIEF Execution mapping DTO with approval state and session version.\nclass ExecutionMappingDto(BaseModel):\n mapping_id: str\n session_id: str\n session_version: int | None = None\n filter_id: str\n variable_id: str\n mapping_method: MappingMethod\n raw_input_value: Any\n effective_value: Any | None = None\n transformation_note: str | None = None\n warning_level: MappingWarningLevel | None = None\n requires_explicit_approval: bool\n approval_state: ApprovalState\n approved_by_user_id: str | None = None\n approved_at: datetime | None = None\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ExecutionMappingDto\n" }, { "contract_id": "HealthSchemas", @@ -40742,7 +41622,7 @@ "contract_type": "Module", "file_path": "backend/src/schemas/profile.py", "start_line": 1, - "end_line": 192, + "end_line": 191, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -40763,7 +41643,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state]\n#\n# @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n#\n# @INVARIANT Schema shapes stay stable for profile UI states and backend preference contracts.\n# @DATA_CONTRACT ProfilePayload -> ProfileSchema\n\nfrom datetime import datetime\nfrom typing import Literal\n\nfrom pydantic import BaseModel, Field\n\n\n# #region ProfilePermissionState [C:3] [TYPE Class]\n# @BRIEF Represents one permission badge state for profile read-only security view.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePermissionState(BaseModel):\n key: str\n allowed: bool\n\n\n# #endregion ProfilePermissionState\n\n\n# #region ProfileSecuritySummary [C:3] [TYPE Class]\n# @BRIEF Read-only security and access snapshot for current user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfileSecuritySummary(BaseModel):\n read_only: bool = True\n auth_source: str | None = None\n current_role: str | None = None\n role_source: str | None = None\n roles: list[str] = Field(default_factory=list)\n permissions: list[ProfilePermissionState] = Field(default_factory=list)\n\n\n# #endregion ProfileSecuritySummary\n\n\n# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ProfilePreference\n\n\n# #region ProfilePreferenceUpdateRequest [C:3] [TYPE Class]\n# @BRIEF Request payload for updating current user's profile settings.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: str | None = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: str | None = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: str | None = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: str | None = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"] | None = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: bool | None = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Literal[\"compact\", \"comfortable\", \"free\"] | None = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: str | None = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: str | None = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: bool | None = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# #endregion ProfilePreferenceUpdateRequest\n\n\n# #region ProfilePreferenceResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for profile preference read/update endpoints.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: str | None = None\n validation_errors: list[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# #endregion ProfilePreferenceResponse\n\n\n# #region SupersetAccountLookupRequest [C:3] [TYPE Class]\n# @BRIEF Query contract for Superset account lookup by selected environment.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: str | None = None\n page_index: int = Field(default=0, ge=0)\n page_size: int = Field(default=20, ge=1, le=100)\n sort_column: str = Field(default=\"username\")\n sort_order: str = Field(default=\"desc\")\n\n\n# #endregion SupersetAccountLookupRequest\n\n\n# #region SupersetAccountCandidate [C:3] [TYPE Class]\n# @BRIEF Canonical account candidate projected from Superset users payload.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountCandidate(BaseModel):\n environment_id: str\n username: str\n display_name: str | None = None\n email: str | None = None\n is_active: bool | None = None\n\n\n# #endregion SupersetAccountCandidate\n\n\n# #region SupersetAccountLookupResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for Superset account lookup (success or degraded mode).\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupResponse(BaseModel):\n status: Literal[\"success\", \"degraded\"]\n environment_id: str\n page_index: int = Field(ge=0)\n page_size: int = Field(ge=1, le=100)\n total: int = Field(ge=0)\n warning: str | None = None\n items: list[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# #endregion SupersetAccountLookupResponse\n\n# #endregion ProfileSchemas\n" + "body": "# #region ProfileSchemas [C:5] [TYPE Module] [SEMANTICS pydantic, profile, schema, superset, profile-permission-state]\n#\n# @BRIEF Defines API schemas for profile preference persistence, security read-only snapshot, and Superset account lookup.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n#\n# @INVARIANT Schema shapes stay stable for profile UI states and backend preference contracts.\n# @DATA_CONTRACT ProfilePayload -> ProfileSchema\n\nfrom datetime import datetime\nfrom typing import Literal\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region ProfilePermissionState [C:3] [TYPE Class]\n# @BRIEF Represents one permission badge state for profile read-only security view.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePermissionState(BaseModel):\n key: str\n allowed: bool\n\n\n# #endregion ProfilePermissionState\n\n\n# #region ProfileSecuritySummary [C:3] [TYPE Class]\n# @BRIEF Read-only security and access snapshot for current user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfileSecuritySummary(BaseModel):\n read_only: bool = True\n auth_source: str | None = None\n current_role: str | None = None\n role_source: str | None = None\n roles: list[str] = Field(default_factory=list)\n permissions: list[ProfilePermissionState] = Field(default_factory=list)\n\n\n# #endregion ProfileSecuritySummary\n\n\n# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ProfilePreference\n\n\n# #region ProfilePreferenceUpdateRequest [C:3] [TYPE Class]\n# @BRIEF Request payload for updating current user's profile settings.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceUpdateRequest(BaseModel):\n superset_username: str | None = Field(\n default=None,\n description=\"Apache Superset username bound to current user profile.\",\n )\n show_only_my_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" can auto-apply profile filter in main context.',\n )\n show_only_slug_dashboards: bool | None = Field(\n default=None,\n description='When true, \"/dashboards\" hides dashboards without slug by default.',\n )\n git_username: str | None = Field(\n default=None,\n description=\"Git author username used for commit signature.\",\n )\n git_email: str | None = Field(\n default=None,\n description=\"Git author email used for commit signature.\",\n )\n git_personal_access_token: str | None = Field(\n default=None,\n description=\"Personal Access Token value. Empty string clears existing token.\",\n )\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\", \"reports-logs\"] | None = Field(\n default=None,\n description=\"Preferred start page after login.\",\n )\n auto_open_task_drawer: bool | None = Field(\n default=None,\n description=\"Auto-open task drawer when long-running tasks start.\",\n )\n dashboards_table_density: Literal[\"compact\", \"comfortable\", \"free\"] | None = (\n Field(\n default=None,\n description=\"Preferred table density for dashboard listings.\",\n )\n )\n telegram_id: str | None = Field(\n default=None,\n description=\"Telegram ID for notifications.\",\n )\n email_address: str | None = Field(\n default=None,\n description=\"Email address for notifications (overrides system email).\",\n )\n notify_on_fail: bool | None = Field(\n default=None,\n description=\"Whether to send notifications on validation failure.\",\n )\n\n\n# #endregion ProfilePreferenceUpdateRequest\n\n\n# #region ProfilePreferenceResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for profile preference read/update endpoints.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreferenceResponse(BaseModel):\n status: Literal[\"success\", \"error\"] = \"success\"\n message: str | None = None\n validation_errors: list[str] = Field(default_factory=list)\n preference: ProfilePreference\n security: ProfileSecuritySummary = Field(default_factory=ProfileSecuritySummary)\n\n\n# #endregion ProfilePreferenceResponse\n\n\n# #region SupersetAccountLookupRequest [C:3] [TYPE Class]\n# @BRIEF Query contract for Superset account lookup by selected environment.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupRequest(BaseModel):\n environment_id: str\n search: str | None = None\n page_index: int = Field(default=0, ge=0)\n page_size: int = Field(default=20, ge=1, le=100)\n sort_column: str = Field(default=\"username\")\n sort_order: str = Field(default=\"desc\")\n\n\n# #endregion SupersetAccountLookupRequest\n\n\n# #region SupersetAccountCandidate [C:3] [TYPE Class]\n# @BRIEF Canonical account candidate projected from Superset users payload.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountCandidate(BaseModel):\n environment_id: str\n username: str\n display_name: str | None = None\n email: str | None = None\n is_active: bool | None = None\n\n\n# #endregion SupersetAccountCandidate\n\n\n# #region SupersetAccountLookupResponse [C:3] [TYPE Class]\n# @BRIEF Response envelope for Superset account lookup (success or degraded mode).\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass SupersetAccountLookupResponse(BaseModel):\n status: Literal[\"success\", \"degraded\"]\n environment_id: str\n page_index: int = Field(ge=0)\n page_size: int = Field(ge=1, le=100)\n total: int = Field(ge=0)\n warning: str | None = None\n items: list[SupersetAccountCandidate] = Field(default_factory=list)\n\n\n# #endregion SupersetAccountLookupResponse\n\n# #endregion ProfileSchemas\n" }, { "contract_id": "ProfilePermissionState", @@ -40820,7 +41700,7 @@ "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", "start_line": 42, - "end_line": 72, + "end_line": 71, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40838,14 +41718,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n class Config:\n from_attributes = True\n\n\n# #endregion ProfilePreference\n" + "body": "# #region ProfilePreference [C:3] [TYPE Class]\n# @BRIEF Represents persisted profile preference for a single authenticated user.\n# @RELATION DEPENDS_ON -> ProfileSchemas\nclass ProfilePreference(BaseModel):\n user_id: str\n superset_username: str | None = None\n superset_username_normalized: str | None = None\n show_only_my_dashboards: bool = False\n show_only_slug_dashboards: bool = True\n\n git_username: str | None = None\n git_email: str | None = None\n has_git_personal_access_token: bool = False\n git_personal_access_token_masked: str | None = None\n\n start_page: Literal[\"dashboards\", \"datasets\", \"reports\"] = \"dashboards\"\n auto_open_task_drawer: bool = True\n dashboards_table_density: Literal[\"compact\", \"comfortable\"] = \"comfortable\"\n\n telegram_id: str | None = None\n email_address: str | None = None\n notify_on_fail: bool = True\n\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ProfilePreference\n" }, { "contract_id": "ProfilePreferenceUpdateRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 75, - "end_line": 131, + "start_line": 74, + "end_line": 130, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40869,8 +41749,8 @@ "contract_id": "ProfilePreferenceResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 134, - "end_line": 145, + "start_line": 133, + "end_line": 144, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40894,8 +41774,8 @@ "contract_id": "SupersetAccountLookupRequest", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 148, - "end_line": 160, + "start_line": 147, + "end_line": 159, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40919,8 +41799,8 @@ "contract_id": "SupersetAccountCandidate", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 163, - "end_line": 174, + "start_line": 162, + "end_line": 173, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40944,8 +41824,8 @@ "contract_id": "SupersetAccountLookupResponse", "contract_type": "Class", "file_path": "backend/src/schemas/profile.py", - "start_line": 177, - "end_line": 190, + "start_line": 176, + "end_line": 189, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40970,7 +41850,7 @@ "contract_type": "Module", "file_path": "backend/src/schemas/settings.py", "start_line": 1, - "end_line": 117, + "end_line": 115, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -40989,7 +41869,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SettingsSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, schema, validate, notification-channel]\n# @BRIEF Pydantic schemas for application settings and automation policies.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime, time\n\nfrom pydantic import BaseModel, Field\n\n\n# #region NotificationChannel [TYPE Class]\n# @BRIEF 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# #endregion NotificationChannel\n\n\n# #region ValidationPolicyBase [TYPE Class]\n# @BRIEF 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: list[NotificationChannel] | None = 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# #endregion ValidationPolicyBase\n\n\n# #region ValidationPolicyCreate [TYPE Class]\n# @BRIEF Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# #endregion ValidationPolicyCreate\n\n\n# #region ValidationPolicyUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n is_active: bool | None = None\n dashboard_ids: list[str] | None = None\n schedule_days: list[int] | None = None\n window_start: time | None = None\n window_end: time | None = None\n notify_owners: bool | None = None\n custom_channels: list[NotificationChannel] | None = None\n alert_condition: str | None = None\n\n\n# #endregion ValidationPolicyUpdate\n\n\n# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF 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# #endregion ValidationPolicyResponse\n\n\n# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion TranslationScheduleItem\n\n# #endregion SettingsSchemas\n" + "body": "# #region SettingsSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, schema, validate, notification-channel]\n# @BRIEF Pydantic schemas for application settings and automation policies.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime, time\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region NotificationChannel [TYPE Class]\n# @BRIEF 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# #endregion NotificationChannel\n\n\n# #region ValidationPolicyBase [TYPE Class]\n# @BRIEF 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: list[NotificationChannel] | None = 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# #endregion ValidationPolicyBase\n\n\n# #region ValidationPolicyCreate [TYPE Class]\n# @BRIEF Schema for creating a new validation policy.\nclass ValidationPolicyCreate(ValidationPolicyBase):\n pass\n\n\n# #endregion ValidationPolicyCreate\n\n\n# #region ValidationPolicyUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing validation policy.\nclass ValidationPolicyUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n is_active: bool | None = None\n dashboard_ids: list[str] | None = None\n schedule_days: list[int] | None = None\n window_start: time | None = None\n window_end: time | None = None\n notify_owners: bool | None = None\n custom_channels: list[NotificationChannel] | None = None\n alert_condition: str | None = None\n\n\n# #endregion ValidationPolicyUpdate\n\n\n# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ValidationPolicyResponse\n\n\n# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion TranslationScheduleItem\n\n# #endregion SettingsSchemas\n" }, { "contract_id": "NotificationChannel", @@ -41064,7 +41944,7 @@ "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", "start_line": 82, - "end_line": 93, + "end_line": 92, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41074,14 +41954,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF 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# #endregion ValidationPolicyResponse\n" + "body": "# #region ValidationPolicyResponse [TYPE Class]\n# @BRIEF Schema for validation policy response data.\nclass ValidationPolicyResponse(ValidationPolicyBase):\n id: str\n created_at: datetime\n updated_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion ValidationPolicyResponse\n" }, { "contract_id": "TranslationScheduleItem", "contract_type": "Class", "file_path": "backend/src/schemas/settings.py", - "start_line": 96, - "end_line": 115, + "start_line": 95, + "end_line": 113, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41092,14 +41972,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n class Config:\n from_attributes = True\n\n\n# #endregion TranslationScheduleItem\n" + "body": "# #region TranslationScheduleItem [C:1] [TYPE Class]\n# @BRIEF Response schema for a translation schedule item with joined job name.\nclass TranslationScheduleItem(BaseModel):\n schedule_id: str\n job_id: str\n job_name: str | None = None\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n\n\n# #endregion TranslationScheduleItem\n" }, { "contract_id": "TranslateSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/translate.py", "start_line": 1, - "end_line": 726, + "end_line": 713, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -41118,7 +41998,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslateSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, translate, schema, translate-job-create]\n# @BRIEF Pydantic v2 schemas for translation API request/response serialization.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field, field_validator\n\n\ndef _validate_bcp47_list(v: list[str] | None) -> list[str] | None:\n \"\"\"Validate that each item in target_languages is a valid BCP-47 tag.\"\"\"\n if not v:\n return v\n for tag in v:\n if not tag or not tag.strip():\n raise ValueError(f\"Invalid BCP-47 tag: '{tag}' — must be a non-empty string\")\n if not re.match(r'^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$', tag.strip()):\n raise ValueError(\n f\"Invalid BCP-47 tag: '{tag}'. \"\n \"Expected format like 'en', 'ru', 'zh-CN', 'pt-BR'.\"\n )\n return v\n\n\n# #region TranslateJobCreate [TYPE Class]\n# @BRIEF Schema for creating a new translation job.\nclass TranslateJobCreate(BaseModel):\n name: str\n description: str | None = 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: str | None = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: str | None = Field(None, description=\"Superset datasource ID\")\n source_table: str | None = Field(None, description=\"Source table name\")\n target_schema: str | None = Field(None, description=\"Target table schema\")\n target_table: str | None = Field(None, description=\"Target table name\")\n source_key_cols: list[str] | None = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n translation_column: str | None = Field(None, description=\"Source column to translate\")\n target_column: str | None = Field(None, description=\"Target column for translated output (defaults to translation_column)\")\n target_language_column: str | None = Field(None, description=\"Target column for language code (e.g. 'ru', 'en')\")\n target_source_column: str | None = Field(None, description=\"Target column for source/original text\")\n target_source_language_column: str | None = Field(None, description=\"Target column for detected source language (BCP-47)\")\n context_columns: list[str] | None = Field(default_factory=list, description=\"Context column names\")\n target_language: str | None = Field(None, description=\"Target language code [DEPRECATED: use target_languages]\")\n target_languages: list[str] | None = Field(default_factory=list, description=\"List of BCP-47 target language codes\")\n provider_id: str | None = Field(None, description=\"LLM provider ID\")\n\n _validate_target_languages = field_validator(\"target_languages\")(_validate_bcp47_list)\n batch_size: int = Field(50, description=\"Records per batch\")\n disable_reasoning: bool = Field(False, description=\"If true, pass reasoning_effort:none to LLM to save output tokens\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: list[str] | None = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: str | None = Field(None, description=\"Superset environment ID\")\n target_database_id: str | None = Field(None, description=\"Superset database ID for SQL Lab insert target\")\n# #endregion TranslateJobCreate\n\n\n# #region TranslateJobUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing translation job.\nclass TranslateJobUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n source_dialect: str | None = None\n target_dialect: str | None = None\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n target_languages: list[str] | None = None\n provider_id: str | None = None\n\n _validate_target_languages = field_validator(\"target_languages\")(_validate_bcp47_list)\n batch_size: int | None = None\n disable_reasoning: bool | None = None\n upsert_strategy: str | None = None\n status: str | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n# #endregion TranslateJobUpdate\n\n\n# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n # source_language removed — deprecated, per-row auto-detected\n target_languages: list[str] | None = None\n provider_id: str | None = None\n batch_size: int = 50\n disable_reasoning: bool = False\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n\n\n# #region DatasourceColumnResponse [TYPE Class]\n# @BRIEF Schema for datasource column metadata response.\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: str | None = None\n is_physical: bool = True\n is_dttm: bool = False\n description: str | None = None\n\n# #endregion DatasourceColumnResponse\n\n# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = 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 [TYPE Class]\n# @BRIEF Schema for duplicate job response.\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 [TYPE Class]\n# @BRIEF Schema for creating a new terminology dictionary.\nclass DictionaryCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n\n\n# #region DictionaryImport [TYPE Class]\n# @BRIEF Schema for importing entries into a terminology dictionary.\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: str | None = 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 default_source_language: str | None = Field(None, description=\"Default BCP-47 source language when column missing in data\")\n default_target_language: str | None = Field(None, description=\"Default BCP-47 target language when column missing in data\")\n# #endregion DictionaryImport\n\n\n# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n\n\n# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry with language pair support.\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: str | None = Field(None, description=\"Optional context notes\")\n source_language: str = Field(\"und\", description=\"BCP-47 source language code (default: und)\")\n target_language: str = Field(\"und\", description=\"BCP-47 target language code (default: und)\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n\n\n# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n source_language: str | None = None\n target_language: str | None = None\n context_notes: str | None = None\n context_data: dict[str, Any] | None = None\n usage_notes: str | None = None\n has_context: bool = False\n context_source: str | None = None\n origin_source_language: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n\n\n# #region DictionaryImportResult [TYPE Class]\n# @BRIEF Schema for dictionary import result.\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 [TYPE Class]\n# @BRIEF Schema for triggering a translation preview.\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: str | None = Field(None, description=\"Optional custom prompt template\")\n env_id: str | None = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n\n\n# #region PreviewRowUpdate [TYPE Class]\n# @BRIEF Schema for approving/editing/rejecting a preview row.\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: str | None = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: str | None = Field(None, description=\"Optional feedback/comment\")\n language_code: str | None = Field(None, description=\"BCP-47 language code for per-language actions (optional, applies to all if omitted)\")\n# #endregion PreviewRowUpdate\n\n\n# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n\n\n# #region CostEstimate [TYPE Class]\n# @BRIEF Schema for cost estimation in preview response.\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n num_languages: int = 1\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 warning: str | None = None\n# #endregion CostEstimate\n\n\n# #region TermCorrectionSubmit [TYPE Class]\n# @BRIEF Schema for submitting a term correction in a translation preview.\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: str | None = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: str | None = Field(None, description=\"Row key within the run\")\n context_data: dict[str, Any] | None = Field(None, description=\"Context data for the dictionary entry\")\n usage_notes: str | None = Field(None, description=\"Usage notes for the dictionary entry\")\n keep_context: bool = Field(True, description=\"If false, clear context_data and set has_context=False on dictionary entry\")\n# #endregion TermCorrectionSubmit\n\n\n# #region TermCorrectionBulkSubmit [TYPE Class]\n# @BRIEF Schema for submitting multiple term corrections atomically.\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 [TYPE Class]\n# @BRIEF Schema for reporting conflicts in correction submission.\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 [TYPE Class]\n# @BRIEF Schema for correction submission response.\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: str | None = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: CorrectionConflictResult | None = None\n message: str | None = None\n# #endregion CorrectionSubmitResponse\n\n\n# #region ScheduleConfig [TYPE Class]\n# @BRIEF Schema for configuring a recurring translation schedule.\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 execution_mode: str = Field(\"full\", description=\"Translation execution mode: 'full' or 'new_key_only'\")\n# #endregion ScheduleConfig\n\n\n# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n\n\n# #region NextExecutionResponse [TYPE Class]\n# @BRIEF Schema for next execution preview.\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 [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n language_stats: list['TranslationRunLanguageStatsResponse'] | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n\n\n# #region RunDetailResponse [TYPE Class]\n# @BRIEF Schema for detailed run response including records, events, and config snapshot.\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: dict[str, Any] | None = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n\n\n# #region RunHistoryFilter [TYPE Class]\n# @BRIEF Schema for filtering run history.\nclass RunHistoryFilter(BaseModel):\n job_id: str | None = None\n status: str | None = None\n trigger_type: str | None = None\n created_by: str | None = None\n date_from: datetime | None = None\n date_to: datetime | None = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n\n\n# #region RunListResponse [TYPE Class]\n# @BRIEF Schema for paginated run list response.\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 [TYPE Class]\n# @BRIEF Schema for aggregated per-job metrics.\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: int | None = None\n cumulative_cost: float | None = None\n avg_duration_ms: float | None = None\n last_run_at: datetime | None = None\n next_scheduled_run: datetime | None = None\n# #endregion AggregatedMetricsResponse\n\n\n# #region TranslationPreviewLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language preview data in API responses.\nclass TranslationPreviewLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewLanguageResponse\n\n\n# #region PreviewRow [TYPE Class]\n# @BRIEF A single row in a translation preview showing original and translated content side by side.\nclass PreviewRow(BaseModel):\n id: str\n source_sql: str | None = None\n target_sql: str | None = None\n source_object_type: str | None = None\n source_object_id: str | None = None\n source_object_name: str | None = None\n status: str = \"PENDING\"\n feedback: str | None = None\n source_language_detected: str | None = None\n needs_review: bool = False\n languages: list[TranslationPreviewLanguageResponse] = Field(default_factory=list, description=\"Per-language translation data\")\n# #endregion PreviewRow\n\n\n# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n cost_estimate: CostEstimate | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n\n\n# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\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: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n\n\n# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\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: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n\n\n# #region TranslationLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language translation data in API responses.\nclass TranslationLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n language_overridden: bool = False\n\n class Config:\n from_attributes = True\n# #endregion TranslationLanguageResponse\n\n\n# #region TranslationRunLanguageStatsResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language statistics in run API responses.\nclass TranslationRunLanguageStatsResponse(BaseModel):\n language_code: str\n total_rows: int = 0\n translated_rows: int = 0\n failed_rows: int = 0\n skipped_rows: int = 0\n token_count: int = 0\n estimated_cost: float = 0.0\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunLanguageStatsResponse\n\n\n# #region OverrideLanguageRequest [C:1] [TYPE Class]\n# @BRIEF Schema for manually overriding the detected source language of a translation.\nclass OverrideLanguageRequest(BaseModel):\n source_language: str = Field(..., description=\"BCP-47 language code to override with\")\n# #endregion OverrideLanguageRequest\n\n\n# #region BulkFindReplaceRequest [C:1] [TYPE Class]\n# @BRIEF Schema for bulk find-and-replace within translations for a target language.\nclass BulkFindReplaceRequest(BaseModel):\n find_pattern: str = Field(..., description=\"Text or regex pattern to find\", max_length=500)\n is_regex: bool = Field(False, description=\"Whether find_pattern is a regular expression\")\n replacement_text: str = Field(..., description=\"Replacement text\")\n target_language: str = Field(..., description=\"BCP-47 language code to target\")\n preview: bool = Field(True, description=\"If true, return matches without applying replacements\")\n submit_to_dictionary: bool = Field(False, description=\"If true, also submit corrections to dictionary\")\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID for submitting corrections\")\n usage_notes: str | None = Field(None, description=\"Usage notes for dictionary entries\")\n submit_to_dictionary_with_context: bool = Field(False, description=\"If true, auto-capture source row context when submitting to dictionary\")\n\n @field_validator(\"find_pattern\")\n @classmethod\n def validate_pattern_length(cls, v: str) -> str:\n \"\"\"Reject patterns longer than 500 characters to prevent ReDoS.\"\"\"\n if len(v) > 500:\n raise ValueError(\"Pattern too long (max 500 characters)\")\n return v\n# #endregion BulkFindReplaceRequest\n\n\n# #region InlineEditRequest [C:1] [TYPE Class]\n# @BRIEF Schema for inline editing a translated value on a completed run result.\nclass InlineEditRequest(BaseModel):\n final_value: str = Field(..., description=\"Corrected/edited translation text\")\n submit_to_dictionary: bool = Field(False, description=\"If true, also save correction to dictionary\")\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID for saving correction\")\n context_data_override: dict[str, Any] | None = Field(None, description=\"Override auto-captured context data for dictionary entry\")\n usage_notes: str | None = Field(None, description=\"Usage notes for the dictionary entry\")\n keep_context: bool = Field(True, description=\"If false, clear context_data and set has_context=False on dictionary entry\")\n# #endregion InlineEditRequest\n\n\n# #region BulkReplacePreviewItem [C:1] [TYPE Class]\n# @BRIEF A single item in a bulk replace preview list.\nclass BulkReplacePreviewItem(BaseModel):\n record_id: str\n language_code: str\n source_term: str\n current_value: str\n new_value: str\n source_language_detected: str | None = None\n# #endregion BulkReplacePreviewItem\n\n\n# #region BulkReplaceResponse [C:1] [TYPE Class]\n# @BRIEF Response for bulk find-and-replace operation.\nclass BulkReplaceResponse(BaseModel):\n rows_affected: int = 0\n corrections_submitted: int = 0\n preview: list[BulkReplacePreviewItem] = Field(default_factory=list)\n# #endregion BulkReplaceResponse\n\n\n# #region InlineCorrectionSubmit [C:1] [TYPE Class]\n# @BRIEF Schema for submitting an inline correction for a specific translated record.\nclass InlineCorrectionSubmit(BaseModel):\n record_id: str = Field(..., description=\"Translation record ID\")\n language_code: str = Field(..., description=\"BCP-47 language code of the translated value\")\n source_term: str = Field(..., description=\"Original source term that was incorrectly translated\")\n corrected_term: str = Field(..., description=\"Corrected translation\")\n dictionary_id: str = Field(..., description=\"Target dictionary ID to also save the correction\")\n source_language: str = Field(..., description=\"BCP-47 source language code\")\n# #endregion InlineCorrectionSubmit\n\n\n# #region TargetSchemaValidationRequest [C:1] [TYPE Class]\n# @BRIEF Schema for requesting target table schema validation.\nclass TargetSchemaValidationRequest(BaseModel):\n environment_id: str = Field(..., description=\"Superset environment ID\")\n target_database_id: str = Field(..., description=\"Superset database ID for the target DB\")\n target_schema: str = Field(\"public\", description=\"Target table schema (default: public)\")\n target_table: str = Field(..., description=\"Target table name\")\n # Column mapping from job config (everything that affects expected columns)\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n target_column: str | None = Field(None, description=\"Target column for translated output\")\n translation_column: str | None = Field(None, description=\"Source column name (fallback for target_column)\")\n target_language_column: str | None = Field(None, description=\"Column for language code\")\n target_source_column: str | None = Field(None, description=\"Column for source text\")\n target_source_language_column: str | None = Field(None, description=\"Column for detected source language\")\n# #endregion TargetSchemaValidationRequest\n\n\n# #region TargetSchemaColumnInfo [C:1] [TYPE Class]\n# @BRIEF Schema for a single column in the schema validation response.\nclass TargetSchemaColumnInfo(BaseModel):\n name: str\n data_type: str | None = None\n is_nullable: bool = True\n# #endregion TargetSchemaColumnInfo\n\n\n# #region TargetSchemaValidationResponse [C:1] [TYPE Class]\n# @BRIEF Schema for target table schema validation response.\nclass TargetSchemaValidationResponse(BaseModel):\n table_exists: bool = False\n error: str | None = None\n expected_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that the INSERT expects\")\n actual_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that exist in the target table\")\n missing_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Expected columns NOT found in target table\")\n extra_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Actual columns NOT in expected list\")\n all_present: bool = False\n# #endregion TargetSchemaValidationResponse\n\n\n# #endregion TranslateSchemas\n" + "body": "# #region TranslateSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, translate, schema, translate-job-create]\n# @BRIEF Pydantic v2 schemas for translation API request/response serialization.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime\nimport re\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict, Field, field_validator\n\n\ndef _validate_bcp47_list(v: list[str] | None) -> list[str] | None:\n \"\"\"Validate that each item in target_languages is a valid BCP-47 tag.\"\"\"\n if not v:\n return v\n for tag in v:\n if not tag or not tag.strip():\n raise ValueError(f\"Invalid BCP-47 tag: '{tag}' — must be a non-empty string\")\n if not re.match(r'^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$', tag.strip()):\n raise ValueError(\n f\"Invalid BCP-47 tag: '{tag}'. \"\n \"Expected format like 'en', 'ru', 'zh-CN', 'pt-BR'.\"\n )\n return v\n\n\n# #region TranslateJobCreate [TYPE Class]\n# @BRIEF Schema for creating a new translation job.\nclass TranslateJobCreate(BaseModel):\n name: str\n description: str | None = 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: str | None = Field(None, description=\"Detected dialect from Superset connection at save time\")\n source_datasource_id: str | None = Field(None, description=\"Superset datasource ID\")\n source_table: str | None = Field(None, description=\"Source table name\")\n target_schema: str | None = Field(None, description=\"Target table schema\")\n target_table: str | None = Field(None, description=\"Target table name\")\n source_key_cols: list[str] | None = Field(default_factory=list, description=\"Source key column names\")\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n translation_column: str | None = Field(None, description=\"Source column to translate\")\n target_column: str | None = Field(None, description=\"Target column for translated output (defaults to translation_column)\")\n target_language_column: str | None = Field(None, description=\"Target column for language code (e.g. 'ru', 'en')\")\n target_source_column: str | None = Field(None, description=\"Target column for source/original text\")\n target_source_language_column: str | None = Field(None, description=\"Target column for detected source language (BCP-47)\")\n context_columns: list[str] | None = Field(default_factory=list, description=\"Context column names\")\n target_language: str | None = Field(None, description=\"Target language code [DEPRECATED: use target_languages]\")\n target_languages: list[str] | None = Field(default_factory=list, description=\"List of BCP-47 target language codes\")\n provider_id: str | None = Field(None, description=\"LLM provider ID\")\n\n _validate_target_languages = field_validator(\"target_languages\")(_validate_bcp47_list)\n batch_size: int = Field(50, description=\"Records per batch\")\n disable_reasoning: bool = Field(False, description=\"If true, pass reasoning_effort:none to LLM to save output tokens\")\n upsert_strategy: str = Field(\"MERGE\", description=\"UPSERT strategy: MERGE, INSERT, UPDATE\")\n dictionary_ids: list[str] | None = Field(default_factory=list, description=\"Associated terminology dictionary IDs\")\n environment_id: str | None = Field(None, description=\"Superset environment ID\")\n target_database_id: str | None = Field(None, description=\"Superset database ID for SQL Lab insert target\")\n# #endregion TranslateJobCreate\n\n\n# #region TranslateJobUpdate [TYPE Class]\n# @BRIEF Schema for updating an existing translation job.\nclass TranslateJobUpdate(BaseModel):\n name: str | None = None\n description: str | None = None\n source_dialect: str | None = None\n target_dialect: str | None = None\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n target_language: str | None = None\n target_languages: list[str] | None = None\n provider_id: str | None = None\n\n _validate_target_languages = field_validator(\"target_languages\")(_validate_bcp47_list)\n batch_size: int | None = None\n disable_reasoning: bool | None = None\n upsert_strategy: str | None = None\n status: str | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n# #endregion TranslateJobUpdate\n\n\n# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n # source_language removed — deprecated, per-row auto-detected\n target_languages: list[str] | None = None\n provider_id: str | None = None\n batch_size: int = 50\n disable_reasoning: bool = False\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslateJobResponse\n\n\n# #region DatasourceColumnResponse [TYPE Class]\n# @BRIEF Schema for datasource column metadata response.\nclass DatasourceColumnResponse(BaseModel):\n name: str\n type: str | None = None\n is_physical: bool = True\n is_dttm: bool = False\n description: str | None = None\n\n# #endregion DatasourceColumnResponse\n\n# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = None\n database_dialect: str\n columns: list[DatasourceColumnResponse] = []\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DatasourceColumnsResponse\n\n\n# #region DuplicateJobResponse [TYPE Class]\n# @BRIEF Schema for duplicate job response.\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DuplicateJobResponse\n\n\n# #region DictionaryCreate [TYPE Class]\n# @BRIEF Schema for creating a new terminology dictionary.\nclass DictionaryCreate(BaseModel):\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool = True\n# #endregion DictionaryCreate\n\n\n# #region DictionaryImport [TYPE Class]\n# @BRIEF Schema for importing entries into a terminology dictionary.\nclass DictionaryImport(BaseModel):\n content: str = Field(..., description=\"CSV or TSV content as raw string\")\n delimiter: str | None = 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 default_source_language: str | None = Field(None, description=\"Default BCP-47 source language when column missing in data\")\n default_target_language: str | None = Field(None, description=\"Default BCP-47 target language when column missing in data\")\n# #endregion DictionaryImport\n\n\n# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryResponse\n\n\n# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry with language pair support.\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: str | None = Field(None, description=\"Optional context notes\")\n source_language: str = Field(\"und\", description=\"BCP-47 source language code (default: und)\")\n target_language: str = Field(\"und\", description=\"BCP-47 target language code (default: und)\")\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryEntryCreate\n\n\n# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n source_language: str | None = None\n target_language: str | None = None\n context_notes: str | None = None\n context_data: dict[str, Any] | None = None\n usage_notes: str | None = None\n has_context: bool = False\n context_source: str | None = None\n origin_source_language: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryEntryResponse\n\n\n# #region DictionaryImportResult [TYPE Class]\n# @BRIEF Schema for dictionary import result.\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 [TYPE Class]\n# @BRIEF Schema for triggering a translation preview.\nclass PreviewRequest(BaseModel):\n sample_size: int = Field(10, ge=1, le=100, description=\"Number of sample rows to preview\")\n prompt_template: str | None = Field(None, description=\"Optional custom prompt template\")\n env_id: str | None = Field(None, description=\"Superset environment ID for preview data fetch\")\n# #endregion PreviewRequest\n\n\n# #region PreviewRowUpdate [TYPE Class]\n# @BRIEF Schema for approving/editing/rejecting a preview row.\nclass PreviewRowUpdate(BaseModel):\n action: str = Field(..., description=\"'approve', 'reject', or 'edit'\")\n translation: str | None = Field(None, description=\"Edited translation (required for 'edit' action)\")\n feedback: str | None = Field(None, description=\"Optional feedback/comment\")\n language_code: str | None = Field(None, description=\"BCP-47 language code for per-language actions (optional, applies to all if omitted)\")\n# #endregion PreviewRowUpdate\n\n\n# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion PreviewAcceptResponse\n\n\n# #region CostEstimate [TYPE Class]\n# @BRIEF Schema for cost estimation in preview response.\nclass CostEstimate(BaseModel):\n sample_size: int = 0\n num_languages: int = 1\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 warning: str | None = None\n# #endregion CostEstimate\n\n\n# #region TermCorrectionSubmit [TYPE Class]\n# @BRIEF Schema for submitting a term correction in a translation preview.\nclass TermCorrectionSubmit(BaseModel):\n source_term: str\n incorrect_target_term: str\n corrected_target_term: str\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID (language-filtered)\")\n origin_run_id: str | None = Field(None, description=\"Run ID from which this correction originated\")\n origin_row_key: str | None = Field(None, description=\"Row key within the run\")\n context_data: dict[str, Any] | None = Field(None, description=\"Context data for the dictionary entry\")\n usage_notes: str | None = Field(None, description=\"Usage notes for the dictionary entry\")\n keep_context: bool = Field(True, description=\"If false, clear context_data and set has_context=False on dictionary entry\")\n# #endregion TermCorrectionSubmit\n\n\n# #region TermCorrectionBulkSubmit [TYPE Class]\n# @BRIEF Schema for submitting multiple term corrections atomically.\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 [TYPE Class]\n# @BRIEF Schema for reporting conflicts in correction submission.\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 [TYPE Class]\n# @BRIEF Schema for correction submission response.\nclass CorrectionSubmitResponse(BaseModel):\n entry_id: str | None = None\n action: str # \"created\", \"updated\", \"conflict_detected\", \"skipped\"\n source_term: str\n target_term: str\n conflict: CorrectionConflictResult | None = None\n message: str | None = None\n# #endregion CorrectionSubmitResponse\n\n\n# #region ScheduleConfig [TYPE Class]\n# @BRIEF Schema for configuring a recurring translation schedule.\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 execution_mode: str = Field(\"full\", description=\"Translation execution mode: 'full' or 'new_key_only'\")\n# #endregion ScheduleConfig\n\n\n# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ScheduleResponse\n\n\n# #region NextExecutionResponse [TYPE Class]\n# @BRIEF Schema for next execution preview.\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 [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n language_stats: list['TranslationRunLanguageStatsResponse'] | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationRunResponse\n\n\n# #region RunDetailResponse [TYPE Class]\n# @BRIEF Schema for detailed run response including records, events, and config snapshot.\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: dict[str, Any] | None = None\n batch_count: int = 0\n# #endregion RunDetailResponse\n\n\n# #region RunHistoryFilter [TYPE Class]\n# @BRIEF Schema for filtering run history.\nclass RunHistoryFilter(BaseModel):\n job_id: str | None = None\n status: str | None = None\n trigger_type: str | None = None\n created_by: str | None = None\n date_from: datetime | None = None\n date_to: datetime | None = None\n page: int = 1\n page_size: int = 20\n# #endregion RunHistoryFilter\n\n\n# #region RunListResponse [TYPE Class]\n# @BRIEF Schema for paginated run list response.\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 [TYPE Class]\n# @BRIEF Schema for aggregated per-job metrics.\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: int | None = None\n cumulative_cost: float | None = None\n avg_duration_ms: float | None = None\n last_run_at: datetime | None = None\n next_scheduled_run: datetime | None = None\n# #endregion AggregatedMetricsResponse\n\n\n# #region TranslationPreviewLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language preview data in API responses.\nclass TranslationPreviewLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationPreviewLanguageResponse\n\n\n# #region PreviewRow [TYPE Class]\n# @BRIEF A single row in a translation preview showing original and translated content side by side.\nclass PreviewRow(BaseModel):\n id: str\n source_sql: str | None = None\n target_sql: str | None = None\n source_object_type: str | None = None\n source_object_id: str | None = None\n source_object_name: str | None = None\n status: str = \"PENDING\"\n feedback: str | None = None\n source_language_detected: str | None = None\n needs_review: bool = False\n languages: list[TranslationPreviewLanguageResponse] = Field(default_factory=list, description=\"Per-language translation data\")\n# #endregion PreviewRow\n\n\n# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n cost_estimate: CostEstimate | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationPreviewResponse\n\n\n# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\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: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationBatchResponse\n\n\n# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\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: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion MetricsResponse\n\n\n# #region TranslationLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language translation data in API responses.\nclass TranslationLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n language_overridden: bool = False\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationLanguageResponse\n\n\n# #region TranslationRunLanguageStatsResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language statistics in run API responses.\nclass TranslationRunLanguageStatsResponse(BaseModel):\n language_code: str\n total_rows: int = 0\n translated_rows: int = 0\n failed_rows: int = 0\n skipped_rows: int = 0\n token_count: int = 0\n estimated_cost: float = 0.0\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationRunLanguageStatsResponse\n\n\n# #region OverrideLanguageRequest [C:1] [TYPE Class]\n# @BRIEF Schema for manually overriding the detected source language of a translation.\nclass OverrideLanguageRequest(BaseModel):\n source_language: str = Field(..., description=\"BCP-47 language code to override with\")\n# #endregion OverrideLanguageRequest\n\n\n# #region BulkFindReplaceRequest [C:1] [TYPE Class]\n# @BRIEF Schema for bulk find-and-replace within translations for a target language.\nclass BulkFindReplaceRequest(BaseModel):\n find_pattern: str = Field(..., description=\"Text or regex pattern to find\", max_length=500)\n is_regex: bool = Field(False, description=\"Whether find_pattern is a regular expression\")\n replacement_text: str = Field(..., description=\"Replacement text\")\n target_language: str = Field(..., description=\"BCP-47 language code to target\")\n preview: bool = Field(True, description=\"If true, return matches without applying replacements\")\n submit_to_dictionary: bool = Field(False, description=\"If true, also submit corrections to dictionary\")\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID for submitting corrections\")\n usage_notes: str | None = Field(None, description=\"Usage notes for dictionary entries\")\n submit_to_dictionary_with_context: bool = Field(False, description=\"If true, auto-capture source row context when submitting to dictionary\")\n\n @field_validator(\"find_pattern\")\n @classmethod\n def validate_pattern_length(cls, v: str) -> str:\n \"\"\"Reject patterns longer than 500 characters to prevent ReDoS.\"\"\"\n if len(v) > 500:\n raise ValueError(\"Pattern too long (max 500 characters)\")\n return v\n# #endregion BulkFindReplaceRequest\n\n\n# #region InlineEditRequest [C:1] [TYPE Class]\n# @BRIEF Schema for inline editing a translated value on a completed run result.\nclass InlineEditRequest(BaseModel):\n final_value: str = Field(..., description=\"Corrected/edited translation text\")\n submit_to_dictionary: bool = Field(False, description=\"If true, also save correction to dictionary\")\n dictionary_id: str | None = Field(None, description=\"Target dictionary ID for saving correction\")\n context_data_override: dict[str, Any] | None = Field(None, description=\"Override auto-captured context data for dictionary entry\")\n usage_notes: str | None = Field(None, description=\"Usage notes for the dictionary entry\")\n keep_context: bool = Field(True, description=\"If false, clear context_data and set has_context=False on dictionary entry\")\n# #endregion InlineEditRequest\n\n\n# #region BulkReplacePreviewItem [C:1] [TYPE Class]\n# @BRIEF A single item in a bulk replace preview list.\nclass BulkReplacePreviewItem(BaseModel):\n record_id: str\n language_code: str\n source_term: str\n current_value: str\n new_value: str\n source_language_detected: str | None = None\n# #endregion BulkReplacePreviewItem\n\n\n# #region BulkReplaceResponse [C:1] [TYPE Class]\n# @BRIEF Response for bulk find-and-replace operation.\nclass BulkReplaceResponse(BaseModel):\n rows_affected: int = 0\n corrections_submitted: int = 0\n preview: list[BulkReplacePreviewItem] = Field(default_factory=list)\n# #endregion BulkReplaceResponse\n\n\n# #region InlineCorrectionSubmit [C:1] [TYPE Class]\n# @BRIEF Schema for submitting an inline correction for a specific translated record.\nclass InlineCorrectionSubmit(BaseModel):\n record_id: str = Field(..., description=\"Translation record ID\")\n language_code: str = Field(..., description=\"BCP-47 language code of the translated value\")\n source_term: str = Field(..., description=\"Original source term that was incorrectly translated\")\n corrected_term: str = Field(..., description=\"Corrected translation\")\n dictionary_id: str = Field(..., description=\"Target dictionary ID to also save the correction\")\n source_language: str = Field(..., description=\"BCP-47 source language code\")\n# #endregion InlineCorrectionSubmit\n\n\n# #region TargetSchemaValidationRequest [C:1] [TYPE Class]\n# @BRIEF Schema for requesting target table schema validation.\nclass TargetSchemaValidationRequest(BaseModel):\n environment_id: str = Field(..., description=\"Superset environment ID\")\n target_database_id: str = Field(..., description=\"Superset database ID for the target DB\")\n target_schema: str = Field(\"public\", description=\"Target table schema (default: public)\")\n target_table: str = Field(..., description=\"Target table name\")\n # Column mapping from job config (everything that affects expected columns)\n target_key_cols: list[str] | None = Field(default_factory=list, description=\"Target key column names\")\n target_column: str | None = Field(None, description=\"Target column for translated output\")\n translation_column: str | None = Field(None, description=\"Source column name (fallback for target_column)\")\n target_language_column: str | None = Field(None, description=\"Column for language code\")\n target_source_column: str | None = Field(None, description=\"Column for source text\")\n target_source_language_column: str | None = Field(None, description=\"Column for detected source language\")\n# #endregion TargetSchemaValidationRequest\n\n\n# #region TargetSchemaColumnInfo [C:1] [TYPE Class]\n# @BRIEF Schema for a single column in the schema validation response.\nclass TargetSchemaColumnInfo(BaseModel):\n name: str\n data_type: str | None = None\n is_nullable: bool = True\n# #endregion TargetSchemaColumnInfo\n\n\n# #region TargetSchemaValidationResponse [C:1] [TYPE Class]\n# @BRIEF Schema for target table schema validation response.\nclass TargetSchemaValidationResponse(BaseModel):\n table_exists: bool = False\n error: str | None = None\n expected_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that the INSERT expects\")\n actual_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that exist in the target table\")\n missing_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Expected columns NOT found in target table\")\n extra_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Actual columns NOT in expected list\")\n all_present: bool = False\n database_name: str | None = Field(default=None, description=\"Actual database name queried (from Superset)\")\n database_backend: str | None = Field(default=None, description=\"Actual database backend dialect (from Superset)\")\n# #endregion TargetSchemaValidationResponse\n\n\n# #endregion TranslateSchemas\n" }, { "contract_id": "TranslateJobCreate", @@ -41159,7 +42039,7 @@ "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", "start_line": 97, - "end_line": 134, + "end_line": 133, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41169,14 +42049,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n # source_language removed — deprecated, per-row auto-detected\n target_languages: list[str] | None = None\n provider_id: str | None = None\n batch_size: int = 50\n disable_reasoning: bool = False\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslateJobResponse\n" + "body": "# #region TranslateJobResponse [TYPE Class]\n# @BRIEF Schema for translation job API responses.\nclass TranslateJobResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n database_dialect: str | None = None\n source_datasource_id: str | None = None\n source_table: str | None = None\n target_schema: str | None = None\n target_table: str | None = None\n source_key_cols: list[str] | None = None\n target_key_cols: list[str] | None = None\n translation_column: str | None = None\n target_column: str | None = None\n target_language_column: str | None = None\n target_source_column: str | None = None\n target_source_language_column: str | None = None\n context_columns: list[str] | None = None\n # source_language removed — deprecated, per-row auto-detected\n target_languages: list[str] | None = None\n provider_id: str | None = None\n batch_size: int = 50\n disable_reasoning: bool = False\n upsert_strategy: str = \"MERGE\"\n status: str\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n dictionary_ids: list[str] | None = None\n environment_id: str | None = None\n target_database_id: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslateJobResponse\n" }, { "contract_id": "DatasourceColumnResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 137, - "end_line": 146, + "start_line": 136, + "end_line": 145, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41192,8 +42072,8 @@ "contract_id": "DatasourceColumnsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 148, - "end_line": 159, + "start_line": 147, + "end_line": 157, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41203,14 +42083,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = None\n database_dialect: str\n columns: list[DatasourceColumnResponse] = []\n\n class Config:\n from_attributes = True\n# #endregion DatasourceColumnsResponse\n" + "body": "# #region DatasourceColumnsResponse [TYPE Class]\n# @BRIEF Schema for datasource columns endpoint response.\nclass DatasourceColumnsResponse(BaseModel):\n datasource_id: int\n datasource_name: str | None = None\n schema_name: str | None = None\n database_dialect: str\n columns: list[DatasourceColumnResponse] = []\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DatasourceColumnsResponse\n" }, { "contract_id": "DuplicateJobResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 162, - "end_line": 171, + "start_line": 160, + "end_line": 168, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41220,14 +42100,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DuplicateJobResponse [TYPE Class]\n# @BRIEF Schema for duplicate job response.\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" + "body": "# #region DuplicateJobResponse [TYPE Class]\n# @BRIEF Schema for duplicate job response.\nclass DuplicateJobResponse(BaseModel):\n id: str\n name: str\n message: str = \"Job duplicated successfully\"\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DuplicateJobResponse\n" }, { "contract_id": "DictionaryCreate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 174, - "end_line": 182, + "start_line": 171, + "end_line": 179, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41243,8 +42123,8 @@ "contract_id": "DictionaryImport", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 185, - "end_line": 194, + "start_line": 182, + "end_line": 191, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41260,8 +42140,8 @@ "contract_id": "DictionaryResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 197, - "end_line": 213, + "start_line": 194, + "end_line": 209, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41271,14 +42151,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryResponse\n" + "body": "# #region DictionaryResponse [TYPE Class]\n# @BRIEF Schema for terminology dictionary API responses.\nclass DictionaryResponse(BaseModel):\n id: str\n name: str\n description: str | None = None\n source_dialect: str\n target_dialect: str\n is_active: bool\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n entry_count: int | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryResponse\n" }, { "contract_id": "DictionaryEntryCreate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 216, - "end_line": 227, + "start_line": 212, + "end_line": 222, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41288,14 +42168,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry with language pair support.\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: str | None = Field(None, description=\"Optional context notes\")\n source_language: str = Field(\"und\", description=\"BCP-47 source language code (default: und)\")\n target_language: str = Field(\"und\", description=\"BCP-47 target language code (default: und)\")\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryCreate\n" + "body": "# #region DictionaryEntryCreate [TYPE Class]\n# @BRIEF Schema for adding/editing a dictionary entry with language pair support.\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: str | None = Field(None, description=\"Optional context notes\")\n source_language: str = Field(\"und\", description=\"BCP-47 source language code (default: und)\")\n target_language: str = Field(\"und\", description=\"BCP-47 target language code (default: und)\")\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryEntryCreate\n" }, { "contract_id": "DictionaryEntryResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 230, - "end_line": 251, + "start_line": 225, + "end_line": 245, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41305,14 +42185,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n source_language: str | None = None\n target_language: str | None = None\n context_notes: str | None = None\n context_data: dict[str, Any] | None = None\n usage_notes: str | None = None\n has_context: bool = False\n context_source: str | None = None\n origin_source_language: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion DictionaryEntryResponse\n" + "body": "# #region DictionaryEntryResponse [TYPE Class]\n# @BRIEF Schema for dictionary entry API responses.\nclass DictionaryEntryResponse(BaseModel):\n id: str\n dictionary_id: str\n source_term: str\n source_term_normalized: str\n target_term: str\n source_language: str | None = None\n target_language: str | None = None\n context_notes: str | None = None\n context_data: dict[str, Any] | None = None\n usage_notes: str | None = None\n has_context: bool = False\n context_source: str | None = None\n origin_source_language: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion DictionaryEntryResponse\n" }, { "contract_id": "DictionaryImportResult", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 254, - "end_line": 263, + "start_line": 248, + "end_line": 257, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41328,8 +42208,8 @@ "contract_id": "PreviewRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 266, - "end_line": 272, + "start_line": 260, + "end_line": 266, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41345,8 +42225,8 @@ "contract_id": "PreviewRowUpdate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 275, - "end_line": 282, + "start_line": 269, + "end_line": 276, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41362,8 +42242,8 @@ "contract_id": "PreviewAcceptResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 285, - "end_line": 299, + "start_line": 279, + "end_line": 292, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41373,14 +42253,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n\n class Config:\n from_attributes = True\n# #endregion PreviewAcceptResponse\n" + "body": "# #region PreviewAcceptResponse [TYPE Class]\n# @BRIEF Schema for preview accept response.\nclass PreviewAcceptResponse(BaseModel):\n id: str\n job_id: str\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list['PreviewRow'] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion PreviewAcceptResponse\n" }, { "contract_id": "CostEstimate", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 302, - "end_line": 315, + "start_line": 295, + "end_line": 308, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41396,8 +42276,8 @@ "contract_id": "TermCorrectionSubmit", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 318, - "end_line": 330, + "start_line": 311, + "end_line": 323, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41413,8 +42293,8 @@ "contract_id": "TermCorrectionBulkSubmit", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 333, - "end_line": 338, + "start_line": 326, + "end_line": 331, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41430,8 +42310,8 @@ "contract_id": "CorrectionConflictResult", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 341, - "end_line": 348, + "start_line": 334, + "end_line": 341, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41447,8 +42327,8 @@ "contract_id": "CorrectionSubmitResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 351, - "end_line": 360, + "start_line": 344, + "end_line": 353, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41464,8 +42344,8 @@ "contract_id": "ScheduleResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 373, - "end_line": 390, + "start_line": 366, + "end_line": 382, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41475,14 +42355,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ScheduleResponse\n" + "body": "# #region ScheduleResponse [TYPE Class]\n# @BRIEF Schema for schedule API responses.\nclass ScheduleResponse(BaseModel):\n id: str\n job_id: str\n cron_expression: str\n timezone: str\n is_active: bool\n execution_mode: str = \"full\"\n last_run_at: datetime | None = None\n next_run_at: datetime | None = None\n created_by: str | None = None\n created_at: datetime\n updated_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ScheduleResponse\n" }, { "contract_id": "NextExecutionResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 393, - "end_line": 400, + "start_line": 385, + "end_line": 392, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41498,8 +42378,8 @@ "contract_id": "TranslationRunResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 403, - "end_line": 429, + "start_line": 395, + "end_line": 420, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41509,14 +42389,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationRunResponse [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n language_stats: list['TranslationRunLanguageStatsResponse'] | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunResponse\n" + "body": "# #region TranslationRunResponse [TYPE Class]\n# @BRIEF Schema for translation run API responses.\nclass TranslationRunResponse(BaseModel):\n id: str\n job_id: str\n status: str\n trigger_type: str | None = None\n started_at: datetime | None = None\n completed_at: datetime | None = None\n error_message: str | None = None\n total_records: int = 0\n successful_records: int = 0\n failed_records: int = 0\n skipped_records: int = 0\n insert_status: str | None = None\n superset_execution_id: str | None = None\n config_snapshot: dict[str, Any] | None = None\n key_hash: str | None = None\n config_hash: str | None = None\n dict_snapshot_hash: str | None = None\n created_by: str | None = None\n created_at: datetime\n language_stats: list['TranslationRunLanguageStatsResponse'] | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationRunResponse\n" }, { "contract_id": "RunDetailResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 432, - "end_line": 440, + "start_line": 423, + "end_line": 431, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41532,8 +42412,8 @@ "contract_id": "RunHistoryFilter", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 443, - "end_line": 454, + "start_line": 434, + "end_line": 445, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41549,8 +42429,8 @@ "contract_id": "RunListResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 457, - "end_line": 464, + "start_line": 448, + "end_line": 455, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41566,8 +42446,8 @@ "contract_id": "AggregatedMetricsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 467, - "end_line": 484, + "start_line": 458, + "end_line": 475, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41583,8 +42463,8 @@ "contract_id": "TranslationPreviewLanguageResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 487, - "end_line": 500, + "start_line": 478, + "end_line": 490, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41595,14 +42475,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationPreviewLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language preview data in API responses.\nclass TranslationPreviewLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewLanguageResponse\n" + "body": "# #region TranslationPreviewLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language preview data in API responses.\nclass TranslationPreviewLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationPreviewLanguageResponse\n" }, { "contract_id": "PreviewRow", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 503, - "end_line": 517, + "start_line": 493, + "end_line": 507, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41618,8 +42498,8 @@ "contract_id": "TranslationPreviewResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 520, - "end_line": 536, + "start_line": 510, + "end_line": 525, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41629,14 +42509,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n cost_estimate: CostEstimate | None = None\n\n class Config:\n from_attributes = True\n# #endregion TranslationPreviewResponse\n" + "body": "# #region TranslationPreviewResponse [TYPE Class]\n# @BRIEF Schema for translation preview session API responses.\nclass TranslationPreviewResponse(BaseModel):\n id: str\n job_id: str\n run_id: str | None = None\n status: str\n created_by: str | None = None\n created_at: datetime\n expires_at: datetime | None = None\n records: list[PreviewRow] = []\n target_languages: list[str] = Field(default_factory=list, description=\"Target languages for this preview\")\n cost_estimate: CostEstimate | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationPreviewResponse\n" }, { "contract_id": "TranslationBatchResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 539, - "end_line": 555, + "start_line": 528, + "end_line": 543, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41646,14 +42526,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\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: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n class Config:\n from_attributes = True\n# #endregion TranslationBatchResponse\n" + "body": "# #region TranslationBatchResponse [TYPE Class]\n# @BRIEF Schema for translation batch API responses.\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: datetime | None = None\n completed_at: datetime | None = None\n created_at: datetime\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationBatchResponse\n" }, { "contract_id": "MetricsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 558, - "end_line": 576, + "start_line": 546, + "end_line": 563, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41663,14 +42543,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\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: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n class Config:\n from_attributes = True\n# #endregion MetricsResponse\n" + "body": "# #region MetricsResponse [TYPE Class]\n# @BRIEF Schema for translation metrics API responses.\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: int | None = None\n p50_duration_ms: int | None = None\n p95_duration_ms: int | None = None\n p99_duration_ms: int | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion MetricsResponse\n" }, { "contract_id": "TranslationLanguageResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 579, - "end_line": 593, + "start_line": 566, + "end_line": 579, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41681,14 +42561,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language translation data in API responses.\nclass TranslationLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n language_overridden: bool = False\n\n class Config:\n from_attributes = True\n# #endregion TranslationLanguageResponse\n" + "body": "# #region TranslationLanguageResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language translation data in API responses.\nclass TranslationLanguageResponse(BaseModel):\n language_code: str\n source_language_detected: str | None = None\n translated_value: str | None = None\n user_edit: str | None = None\n final_value: str | None = None\n status: str = \"pending\"\n needs_review: bool = False\n language_overridden: bool = False\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationLanguageResponse\n" }, { "contract_id": "TranslationRunLanguageStatsResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 596, - "end_line": 609, + "start_line": 582, + "end_line": 594, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41699,14 +42579,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TranslationRunLanguageStatsResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language statistics in run API responses.\nclass TranslationRunLanguageStatsResponse(BaseModel):\n language_code: str\n total_rows: int = 0\n translated_rows: int = 0\n failed_rows: int = 0\n skipped_rows: int = 0\n token_count: int = 0\n estimated_cost: float = 0.0\n\n class Config:\n from_attributes = True\n# #endregion TranslationRunLanguageStatsResponse\n" + "body": "# #region TranslationRunLanguageStatsResponse [C:1] [TYPE Class]\n# @BRIEF Schema for per-language statistics in run API responses.\nclass TranslationRunLanguageStatsResponse(BaseModel):\n language_code: str\n total_rows: int = 0\n translated_rows: int = 0\n failed_rows: int = 0\n skipped_rows: int = 0\n token_count: int = 0\n estimated_cost: float = 0.0\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion TranslationRunLanguageStatsResponse\n" }, { "contract_id": "OverrideLanguageRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 612, - "end_line": 616, + "start_line": 597, + "end_line": 601, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41723,8 +42603,8 @@ "contract_id": "BulkFindReplaceRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 619, - "end_line": 639, + "start_line": 604, + "end_line": 624, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41741,8 +42621,8 @@ "contract_id": "InlineEditRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 642, - "end_line": 651, + "start_line": 627, + "end_line": 636, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41759,8 +42639,8 @@ "contract_id": "BulkReplacePreviewItem", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 654, - "end_line": 663, + "start_line": 639, + "end_line": 648, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41777,8 +42657,8 @@ "contract_id": "BulkReplaceResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 666, - "end_line": 672, + "start_line": 651, + "end_line": 657, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41795,8 +42675,8 @@ "contract_id": "InlineCorrectionSubmit", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 675, - "end_line": 684, + "start_line": 660, + "end_line": 669, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41813,8 +42693,8 @@ "contract_id": "TargetSchemaValidationRequest", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 687, - "end_line": 701, + "start_line": 672, + "end_line": 686, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41831,8 +42711,8 @@ "contract_id": "TargetSchemaColumnInfo", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 704, - "end_line": 710, + "start_line": 689, + "end_line": 695, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41849,8 +42729,8 @@ "contract_id": "TargetSchemaValidationResponse", "contract_type": "Class", "file_path": "backend/src/schemas/translate.py", - "start_line": 713, - "end_line": 723, + "start_line": 698, + "end_line": 710, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41861,14 +42741,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TargetSchemaValidationResponse [C:1] [TYPE Class]\n# @BRIEF Schema for target table schema validation response.\nclass TargetSchemaValidationResponse(BaseModel):\n table_exists: bool = False\n error: str | None = None\n expected_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that the INSERT expects\")\n actual_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that exist in the target table\")\n missing_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Expected columns NOT found in target table\")\n extra_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Actual columns NOT in expected list\")\n all_present: bool = False\n# #endregion TargetSchemaValidationResponse\n" + "body": "# #region TargetSchemaValidationResponse [C:1] [TYPE Class]\n# @BRIEF Schema for target table schema validation response.\nclass TargetSchemaValidationResponse(BaseModel):\n table_exists: bool = False\n error: str | None = None\n expected_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that the INSERT expects\")\n actual_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Columns that exist in the target table\")\n missing_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Expected columns NOT found in target table\")\n extra_columns: list[TargetSchemaColumnInfo] = Field(default_factory=list, description=\"Actual columns NOT in expected list\")\n all_present: bool = False\n database_name: str | None = Field(default=None, description=\"Actual database name queried (from Superset)\")\n database_backend: str | None = Field(default=None, description=\"Actual database backend dialect (from Superset)\")\n# #endregion TargetSchemaValidationResponse\n" }, { "contract_id": "ValidationSchemas", "contract_type": "Module", "file_path": "backend/src/schemas/validation.py", "start_line": 1, - "end_line": 140, + "end_line": 137, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -41887,7 +42767,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, validation, schema, task, run]\n# @BRIEF Pydantic v2 schemas for validation task management and run history API serialization.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, Field\n\n\n# #region ValidationTaskCreate [C:1] [TYPE Class]\n# @BRIEF Schema for creating a new validation task (policy).\nclass ValidationTaskCreate(BaseModel):\n name: str = Field(..., description=\"Human-readable task name\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n dashboard_ids: list[str] = Field(..., description=\"List of dashboard IDs to validate (use single-element array for v1)\")\n provider_id: str = Field(..., description=\"LLM provider ID — must be multimodal\")\n schedule_days: list[int] | None = Field(None, description=\"Days of week (0-6, 0=Sunday). Omit or empty for manual-only.\")\n window_start: str | None = Field(None, description=\"Window start time, HH:MM format\")\n window_end: str | None = Field(None, description=\"Window end time, HH:MM format\")\n notify_owners: bool = Field(True, description=\"Notify dashboard owners on failure\")\n custom_channels: list[str] | None = Field(None, description=\"Additional notification channels\")\n alert_condition: str = Field(\"FAIL_ONLY\", description=\"FAIL_ONLY, WARN_AND_FAIL, or ALWAYS\")\n is_active: bool = Field(True, description=\"Whether the task is active\")\n# #endregion ValidationTaskCreate\n\n\n# #region ValidationTaskUpdate [C:1] [TYPE Class]\n# @BRIEF Schema for updating an existing validation task (policy).\nclass ValidationTaskUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n dashboard_ids: list[str] | None = None\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool | None = None\n custom_channels: list[str] | None = None\n alert_condition: str | None = None\n is_active: bool | None = None\n# #endregion ValidationTaskUpdate\n\n\n# #region ValidationTaskResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation task API responses — includes last run status from join.\nclass ValidationTaskResponse(BaseModel):\n id: str\n name: str\n environment_id: str\n is_active: bool\n dashboard_ids: list[str]\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool = True\n custom_channels: list[str] | None = None\n alert_condition: str = \"FAIL_ONLY\"\n created_at: datetime\n updated_at: datetime | None = None\n last_run_status: str | None = None\n last_run_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ValidationTaskResponse\n\n\n# #region ValidationTaskListResponse [C:1] [TYPE Class]\n# @BRIEF Schema for paginated task list response.\nclass ValidationTaskListResponse(BaseModel):\n items: list[ValidationTaskResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion ValidationTaskListResponse\n\n\n# #region ValidationRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation run history listing.\nclass ValidationRunResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n task_name: str | None = None # resolved from ValidationPolicy at query time\n issues_count: int = 0 # number of issues found during validation\n\n class Config:\n from_attributes = True\n# #endregion ValidationRunResponse\n\n\n# #region ValidationRunListResponse [C:1] [TYPE Class]\n# @BRIEF Schema for paginated run list response.\nclass ValidationRunListResponse(BaseModel):\n items: list[ValidationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion ValidationRunListResponse\n\n\n# #region ValidationRunDetailResponse [C:1] [TYPE Class]\n# @BRIEF Schema for full run detail including parsed issues and raw response.\nclass ValidationRunDetailResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n issues: list[dict[str, Any]] = Field(default_factory=list)\n raw_response: str | None = None\n task_name: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion ValidationRunDetailResponse\n\n\n# #region TriggerRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for trigger-run response — returns spawned task ID.\nclass TriggerRunResponse(BaseModel):\n task_id: str\n spawned_task_id: str\n status: str = \"PENDING\"\n message: str = \"Validation task spawned\"\n# #endregion TriggerRunResponse\n\n\n# #endregion ValidationSchemas\n" + "body": "# #region ValidationSchemas [C:3] [TYPE Module] [SEMANTICS pydantic, validation, schema, task, run]\n# @BRIEF Pydantic v2 schemas for validation task management and run history API serialization.\n# @LAYER API\n# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\n# #region ValidationTaskCreate [C:1] [TYPE Class]\n# @BRIEF Schema for creating a new validation task (policy).\nclass ValidationTaskCreate(BaseModel):\n name: str = Field(..., description=\"Human-readable task name\")\n environment_id: str = Field(..., description=\"Target Superset environment ID\")\n dashboard_ids: list[str] = Field(..., description=\"List of dashboard IDs to validate (use single-element array for v1)\")\n provider_id: str = Field(..., description=\"LLM provider ID — must be multimodal\")\n schedule_days: list[int] | None = Field(None, description=\"Days of week (0-6, 0=Sunday). Omit or empty for manual-only.\")\n window_start: str | None = Field(None, description=\"Window start time, HH:MM format\")\n window_end: str | None = Field(None, description=\"Window end time, HH:MM format\")\n notify_owners: bool = Field(True, description=\"Notify dashboard owners on failure\")\n custom_channels: list[str] | None = Field(None, description=\"Additional notification channels\")\n alert_condition: str = Field(\"FAIL_ONLY\", description=\"FAIL_ONLY, WARN_AND_FAIL, or ALWAYS\")\n is_active: bool = Field(True, description=\"Whether the task is active\")\n# #endregion ValidationTaskCreate\n\n\n# #region ValidationTaskUpdate [C:1] [TYPE Class]\n# @BRIEF Schema for updating an existing validation task (policy).\nclass ValidationTaskUpdate(BaseModel):\n name: str | None = None\n environment_id: str | None = None\n dashboard_ids: list[str] | None = None\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool | None = None\n custom_channels: list[str] | None = None\n alert_condition: str | None = None\n is_active: bool | None = None\n# #endregion ValidationTaskUpdate\n\n\n# #region ValidationTaskResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation task API responses — includes last run status from join.\nclass ValidationTaskResponse(BaseModel):\n id: str\n name: str\n environment_id: str\n is_active: bool\n dashboard_ids: list[str]\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool = True\n custom_channels: list[str] | None = None\n alert_condition: str = \"FAIL_ONLY\"\n created_at: datetime\n updated_at: datetime | None = None\n last_run_status: str | None = None\n last_run_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationTaskResponse\n\n\n# #region ValidationTaskListResponse [C:1] [TYPE Class]\n# @BRIEF Schema for paginated task list response.\nclass ValidationTaskListResponse(BaseModel):\n items: list[ValidationTaskResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion ValidationTaskListResponse\n\n\n# #region ValidationRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation run history listing.\nclass ValidationRunResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n task_name: str | None = None # resolved from ValidationPolicy at query time\n issues_count: int = 0 # number of issues found during validation\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationRunResponse\n\n\n# #region ValidationRunListResponse [C:1] [TYPE Class]\n# @BRIEF Schema for paginated run list response.\nclass ValidationRunListResponse(BaseModel):\n items: list[ValidationRunResponse] = []\n total: int = 0\n page: int = 1\n page_size: int = 20\n# #endregion ValidationRunListResponse\n\n\n# #region ValidationRunDetailResponse [C:1] [TYPE Class]\n# @BRIEF Schema for full run detail including parsed issues and raw response.\nclass ValidationRunDetailResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n issues: list[dict[str, Any]] = Field(default_factory=list)\n raw_response: str | None = None\n task_name: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationRunDetailResponse\n\n\n# #region TriggerRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for trigger-run response — returns spawned task ID.\nclass TriggerRunResponse(BaseModel):\n task_id: str\n spawned_task_id: str\n status: str = \"PENDING\"\n message: str = \"Validation task spawned\"\n# #endregion TriggerRunResponse\n\n\n# #endregion ValidationSchemas\n" }, { "contract_id": "ValidationTaskCreate", @@ -41930,7 +42810,7 @@ "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", "start_line": 46, - "end_line": 68, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41941,14 +42821,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationTaskResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation task API responses — includes last run status from join.\nclass ValidationTaskResponse(BaseModel):\n id: str\n name: str\n environment_id: str\n is_active: bool\n dashboard_ids: list[str]\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool = True\n custom_channels: list[str] | None = None\n alert_condition: str = \"FAIL_ONLY\"\n created_at: datetime\n updated_at: datetime | None = None\n last_run_status: str | None = None\n last_run_at: datetime | None = None\n\n class Config:\n from_attributes = True\n# #endregion ValidationTaskResponse\n" + "body": "# #region ValidationTaskResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation task API responses — includes last run status from join.\nclass ValidationTaskResponse(BaseModel):\n id: str\n name: str\n environment_id: str\n is_active: bool\n dashboard_ids: list[str]\n provider_id: str | None = None\n schedule_days: list[int] | None = None\n window_start: str | None = None\n window_end: str | None = None\n notify_owners: bool = True\n custom_channels: list[str] | None = None\n alert_condition: str = \"FAIL_ONLY\"\n created_at: datetime\n updated_at: datetime | None = None\n last_run_status: str | None = None\n last_run_at: datetime | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationTaskResponse\n" }, { "contract_id": "ValidationTaskListResponse", "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", - "start_line": 71, - "end_line": 78, + "start_line": 70, + "end_line": 77, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41965,8 +42845,8 @@ "contract_id": "ValidationRunResponse", "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", - "start_line": 81, - "end_line": 97, + "start_line": 80, + "end_line": 95, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -41977,14 +42857,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation run history listing.\nclass ValidationRunResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n task_name: str | None = None # resolved from ValidationPolicy at query time\n issues_count: int = 0 # number of issues found during validation\n\n class Config:\n from_attributes = True\n# #endregion ValidationRunResponse\n" + "body": "# #region ValidationRunResponse [C:1] [TYPE Class]\n# @BRIEF Schema for validation run history listing.\nclass ValidationRunResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n task_name: str | None = None # resolved from ValidationPolicy at query time\n issues_count: int = 0 # number of issues found during validation\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationRunResponse\n" }, { "contract_id": "ValidationRunListResponse", "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", - "start_line": 100, - "end_line": 107, + "start_line": 98, + "end_line": 105, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42001,8 +42881,8 @@ "contract_id": "ValidationRunDetailResponse", "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", - "start_line": 110, - "end_line": 127, + "start_line": 108, + "end_line": 124, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -42013,14 +42893,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationRunDetailResponse [C:1] [TYPE Class]\n# @BRIEF Schema for full run detail including parsed issues and raw response.\nclass ValidationRunDetailResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n issues: list[dict[str, Any]] = Field(default_factory=list)\n raw_response: str | None = None\n task_name: str | None = None\n\n class Config:\n from_attributes = True\n# #endregion ValidationRunDetailResponse\n" + "body": "# #region ValidationRunDetailResponse [C:1] [TYPE Class]\n# @BRIEF Schema for full run detail including parsed issues and raw response.\nclass ValidationRunDetailResponse(BaseModel):\n id: str\n task_id: str | None = None\n dashboard_id: str\n environment_id: str | None = None\n status: str\n summary: str = \"\"\n timestamp: datetime | None = None\n screenshot_path: str | None = None\n issues: list[dict[str, Any]] = Field(default_factory=list)\n raw_response: str | None = None\n task_name: str | None = None\n\n model_config = ConfigDict(from_attributes=True)\n# #endregion ValidationRunDetailResponse\n" }, { "contract_id": "TriggerRunResponse", "contract_type": "Class", "file_path": "backend/src/schemas/validation.py", - "start_line": 130, - "end_line": 137, + "start_line": 127, + "end_line": 134, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -43030,7 +43910,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt]\n# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [verify_password]\n# @RELATION DEPENDS_ON -> [create_access_token]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\n# @INVARIANT Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.\n# @PRE Core auth models and security utilities available.\n# @POST User identity verified and session tokens issued according to role scopes.\n# @SIDE_EFFECT Writes last login timestamps and JIT-provisions external users.\n# @DATA_CONTRACT [Credentials | ADFSClaims] -> [UserEntity | SessionToken]\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.jwt import create_access_token\nfrom ..core.auth.logger import log_security_event\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.auth.security import verify_password\nfrom ..core.logger import belief_scope\nfrom ..models.auth import User\n\n\n# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT Input(Session) -> Model(AuthRepository)\n # @PARAM db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE username and password are non-empty credential inputs.\n # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT Input(str username, str password) -> Output(User | None)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION CALLS ->[verify_password]\n # @RELATION DEPENDS_ON ->[User]\n # @PARAM username (str) - The username.\n # @PARAM password (str) - The plain password.\n # @RETURN Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE user is a valid User entity containing username and iterable roles with role.name values.\n # @POST Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION CALLS ->[create_access_token]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user (User) - The authenticated user.\n # @RETURN Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n\n# #endregion auth_service\n" + "body": "# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt]\n# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [verify_password]\n# @RELATION DEPENDS_ON -> [create_access_token]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\n# @INVARIANT Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.\n# @PRE Core auth models and security utilities available.\n# @POST User identity verified and session tokens issued according to role scopes.\n# @SIDE_EFFECT Writes last login timestamps and JIT-provisions external users.\n# @DATA_CONTRACT [Credentials | ADFSClaims] -> [UserEntity | SessionToken]\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.jwt import create_access_token\nfrom ..core.auth.logger import log_security_event\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.auth.security import verify_password\nfrom ..core.logger import belief_scope\nfrom ..models.auth import User\n\n\n# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT Input(Session) -> Model(AuthRepository)\n # @PARAM db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE username and password are non-empty credential inputs.\n # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT Input(str username, str password) -> Output(User | None)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION CALLS ->[verify_password]\n # @RELATION DEPENDS_ON ->[User]\n # @PARAM username (str) - The username.\n # @PARAM password (str) - The plain password.\n # @RETURN Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE user is a valid User entity containing username and iterable roles with role.name values.\n # @POST Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION CALLS ->[create_access_token]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user (User) - The authenticated user.\n # @RETURN Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n\n# #endregion auth_service\n" }, { "contract_id": "AuthService", @@ -43067,7 +43947,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT Input(Session) -> Model(AuthRepository)\n # @PARAM db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE username and password are non-empty credential inputs.\n # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT Input(str username, str password) -> Output(User | None)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION CALLS ->[verify_password]\n # @RELATION DEPENDS_ON ->[User]\n # @PARAM username (str) - The username.\n # @PARAM password (str) - The plain password.\n # @RETURN Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE user is a valid User entity containing username and iterable roles with role.name values.\n # @POST Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION CALLS ->[create_access_token]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user (User) - The authenticated user.\n # @RETURN Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.utcnow()\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n" + "body": "# #region AuthService [C:3] [TYPE Class]\n# @BRIEF Provides high-level authentication services.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [Role]\nclass AuthService:\n # region AuthService_init [TYPE Function]\n # @PURPOSE: Initializes the authentication service with repository access over an active DB session.\n # @PRE db is a valid SQLAlchemy Session instance bound to the auth persistence context.\n # @POST self.repo is initialized and ready for auth user/role CRUD operations.\n # @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session.\n # @DATA_CONTRACT Input(Session) -> Model(AuthRepository)\n # @PARAM db (Session) - SQLAlchemy session.\n def __init__(self, db: Session):\n self.db = db\n self.repo = AuthRepository(db)\n\n # endregion AuthService_init\n\n # region AuthService.authenticate_user [TYPE Function]\n # @PURPOSE: Validates credentials and account state for local username/password authentication.\n # @PRE username and password are non-empty credential inputs.\n # @POST Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.\n # @SIDE_EFFECT Persists last_login update for successful authentications via repository.\n # @DATA_CONTRACT Input(str username, str password) -> Output(User | None)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION CALLS ->[verify_password]\n # @RELATION DEPENDS_ON ->[User]\n # @PARAM username (str) - The username.\n # @PARAM password (str) - The plain password.\n # @RETURN Optional[User] - The authenticated user or None.\n def authenticate_user(self, username: str, password: str) -> User | None:\n with belief_scope(\"auth.authenticate_user\"):\n user = self.repo.get_user_by_username(username)\n if not user or not user.is_active:\n return None\n\n if not verify_password(password, user.password_hash):\n return None\n\n # Update last login\n user.last_login = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.authenticate_user\n\n # region AuthService.create_session [TYPE Function]\n # @PURPOSE: Issues an access token payload for an already authenticated user.\n # @PRE user is a valid User entity containing username and iterable roles with role.name values.\n # @POST Returns session dict with non-empty access_token and token_type='bearer'.\n # @SIDE_EFFECT Generates signed JWT via auth JWT provider.\n # @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type})\n # @RELATION CALLS ->[create_access_token]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user (User) - The authenticated user.\n # @RETURN Dict[str, str] - Session data.\n def create_session(self, user: User) -> dict[str, str]:\n with belief_scope(\"auth.create_session\"):\n roles = [role.name for role in user.roles]\n access_token = create_access_token(\n data={\"sub\": user.username, \"scopes\": roles}\n )\n return {\"access_token\": access_token, \"token_type\": \"bearer\"}\n\n # endregion AuthService.create_session\n\n # region AuthService.provision_adfs_user [TYPE Function]\n # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.\n # @PRE user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.\n # @POST Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.\n # @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state.\n # @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)\n # @RELATION DEPENDS_ON ->[AuthRepository]\n # @RELATION DEPENDS_ON ->[User]\n # @RELATION DEPENDS_ON ->[Role]\n # @PARAM user_info (Dict[str, Any]) - Claims from ADFS token.\n # @RETURN User - The provisioned user.\n def provision_adfs_user(self, user_info: dict[str, Any]) -> User:\n with belief_scope(\"auth.provision_adfs_user\"):\n username = user_info.get(\"upn\") or user_info.get(\"email\")\n email = user_info.get(\"email\")\n groups = user_info.get(\"groups\", [])\n\n user = self.repo.get_user_by_username(username)\n if not user:\n user = User(\n username=username,\n email=email,\n full_name=user_info.get(\"name\"),\n auth_source=\"ADFS\",\n is_active=True,\n is_ad_user=True,\n )\n self.db.add(user)\n log_security_event(\"USER_PROVISIONED\", username, {\"source\": \"ADFS\"})\n\n # Sync roles from AD groups\n mapped_roles = self.repo.get_roles_by_ad_groups(groups)\n user.roles = mapped_roles\n\n user.last_login = datetime.now(UTC)\n self.db.commit()\n self.db.refresh(user)\n\n return user\n\n # endregion AuthService.provision_adfs_user\n\n\n# #endregion AuthService\n" }, { "contract_id": "CleanReleaseContracts", @@ -45143,7 +46023,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationEngine [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, clarification, finding, resolution]\n# @BRIEF Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\n# @RELATION DEPENDS_ON -> [ClarificationAnswer]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DISPATCHES -> [ClarificationHelpers]\n# @PRE Target session contains a persisted clarification aggregate in the current ownership scope.\n# @POST Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.\n# @SIDE_EFFECT Persists clarification answers, question/session states, and related readiness/finding changes.\n# @DATA_CONTRACT Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]\n# @INVARIANT Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.\n# @RATIONALE Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.\n# @REJECTED Keeping all clarification logic in one file because it exceeded the fractal limit.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationAnswer,\n ClarificationQuestion,\n ClarificationSession,\n ClarificationStatus,\n DatasetReviewSession,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n ValidationFinding,\n)\nfrom src.services.dataset_review.clarification_pkg._helpers import (\n build_impact_summary,\n count_remaining_questions,\n count_resolved_questions,\n derive_readiness_state,\n derive_recommended_action,\n normalize_answer_value,\n select_next_open_question,\n upsert_clarification_finding,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\n\n\n# #region ClarificationQuestionPayload [C:2] [TYPE Class]\n# @BRIEF Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# #endregion ClarificationQuestionPayload\n\n\n# #region ClarificationStateResult [C:2] [TYPE Class]\n# @BRIEF Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: ClarificationQuestionPayload | None\n session: DatasetReviewSession\n changed_findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion ClarificationStateResult\n\n\n# #region ClarificationAnswerCommand [C:2] [TYPE Class]\n# @BRIEF Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None\n user: User\n\n\n# #endregion ClarificationAnswerCommand\n\n\n# #region ClarificationEngine [C:4] [TYPE Class]\n# @BRIEF Provide deterministic one-question-at-a-time clarification selection and answer persistence.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION CALLS -> [ClarificationHelpers]\n# @PRE Repository is bound to the current request transaction scope.\n# @POST Returned clarification state is persistence-backed and aligned with session readiness/recommended action.\n# @SIDE_EFFECT Mutates clarification answers, session flags, and related clarification findings.\nclass ClarificationEngine:\n # region ClarificationEngine_init [TYPE Function]\n # @PURPOSE: Bind repository dependency for clarification persistence operations.\n def __init__(self, repository: DatasetReviewSessionRepository) -> None:\n self.repository = repository\n\n # endregion ClarificationEngine_init\n\n # region build_question_payload [TYPE Function]\n # @PURPOSE: Return the one active highest-priority clarification question payload.\n # @PRE Session contains unresolved clarification state or a resumable clarification session.\n # @POST Returns exactly one active/open question payload or None when no unresolved question remains.\n # @SIDE_EFFECT Normalizes the active-question pointer and clarification status in persistence.\n def build_question_payload(\n self, session: DatasetReviewSession,\n ) -> ClarificationQuestionPayload | None:\n with belief_scope(\"ClarificationEngine.build_question_payload\"):\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.reason(\"No clarification session found\", extra={\"session_id\": session.session_id})\n return None\n\n active_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n active_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n\n if not active_questions:\n clarification_session.current_question_id = None\n clarification_session.status = ClarificationStatus.COMPLETED\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n if session.current_phase == SessionPhase.CLARIFICATION:\n session.current_phase = SessionPhase.REVIEW\n self.repository.db.commit()\n logger.reflect(\"No unresolved clarification question remains\", extra={\"session_id\": session.session_id})\n return None\n\n selected_question = active_questions[0]\n clarification_session.current_question_id = selected_question.question_id\n clarification_session.status = ClarificationStatus.ACTIVE\n session.readiness_state = ReadinessState.CLARIFICATION_ACTIVE\n session.recommended_action = RecommendedAction.ANSWER_NEXT_QUESTION\n session.current_phase = SessionPhase.CLARIFICATION\n\n logger.reason(\"Selected active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": selected_question.question_id, \"priority\": selected_question.priority})\n self.repository.db.commit()\n\n payload = ClarificationQuestionPayload(\n question_id=selected_question.question_id,\n clarification_session_id=selected_question.clarification_session_id,\n topic_ref=selected_question.topic_ref,\n question_text=selected_question.question_text,\n why_it_matters=selected_question.why_it_matters,\n current_guess=selected_question.current_guess,\n priority=selected_question.priority,\n state=selected_question.state,\n options=[\n {\"option_id\": o.option_id, \"question_id\": o.question_id, \"label\": o.label, \"value\": o.value, \"is_recommended\": o.is_recommended, \"display_order\": o.display_order}\n for o in sorted(selected_question.options, key=lambda item: (item.display_order, item.label, item.option_id))\n ],\n )\n logger.reflect(\"Clarification payload built\", extra={\"session_id\": session.session_id, \"question_id\": payload.question_id, \"option_count\": len(payload.options)})\n return payload\n\n # endregion build_question_payload\n\n # region record_answer [TYPE Function]\n # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.\n # @PRE Target question belongs to the session's active clarification session and is still open.\n # @POST Answer row is persisted before current-question pointer advances.\n # @SIDE_EFFECT Inserts answer row, mutates question/session states, updates clarification findings, and commits.\n def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:\n with belief_scope(\"ClarificationEngine.record_answer\"):\n session = command.session\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.explore(\"Cannot record clarification answer because no clarification session exists\", extra={\"session_id\": session.session_id})\n raise ValueError(\"Clarification session not found\")\n\n question = self._find_question(clarification_session, command.question_id)\n if question is None:\n logger.explore(\"Cannot record clarification answer for foreign or missing question\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question not found\")\n\n if question.answer is not None:\n logger.explore(\"Rejected duplicate clarification answer submission\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question already answered\")\n\n if clarification_session.current_question_id and clarification_session.current_question_id != question.question_id:\n logger.explore(\"Rejected answer for non-active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"current_question_id\": clarification_session.current_question_id})\n raise ValueError(\"Only the active clarification question can be answered\")\n\n normalized_answer_value = normalize_answer_value(command.answer_kind, command.answer_value, question)\n\n logger.reason(\"Persisting clarification answer before state advancement\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"answer_kind\": command.answer_kind.value})\n persisted_answer = ClarificationAnswer(\n question_id=question.question_id,\n answer_kind=command.answer_kind,\n answer_value=normalized_answer_value,\n answered_by_user_id=command.user.id,\n impact_summary=build_impact_summary(question, command.answer_kind, normalized_answer_value),\n )\n self.repository.db.add(persisted_answer)\n self.repository.db.flush()\n\n changed_finding = upsert_clarification_finding(\n session=session, question=question, answer_kind=command.answer_kind,\n answer_value=normalized_answer_value, db_session=self.repository.db,\n )\n\n if command.answer_kind == AnswerKind.SELECTED or command.answer_kind == AnswerKind.CUSTOM:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.SKIPPED:\n question.state = QuestionState.SKIPPED\n elif command.answer_kind == AnswerKind.EXPERT_REVIEW:\n question.state = QuestionState.EXPERT_REVIEW\n\n question.updated_at = datetime.utcnow()\n self.repository.db.flush()\n\n clarification_session.resolved_count = count_resolved_questions(clarification_session)\n clarification_session.remaining_count = count_remaining_questions(clarification_session)\n clarification_session.summary_delta = self.summarize_progress(clarification_session)\n clarification_session.updated_at = datetime.utcnow()\n\n next_question = select_next_open_question(clarification_session)\n clarification_session.current_question_id = next_question.question_id if next_question else None\n clarification_session.status = ClarificationStatus.ACTIVE if next_question else ClarificationStatus.COMPLETED\n if clarification_session.status == ClarificationStatus.COMPLETED:\n clarification_session.completed_at = datetime.utcnow()\n\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n session.current_phase = SessionPhase.CLARIFICATION if clarification_session.current_question_id else SessionPhase.REVIEW\n self.repository.bump_session_version(session)\n\n self.repository.db.commit()\n self.repository.db.refresh(session)\n\n logger.reflect(\"Clarification answer recorded and session advanced\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"next_question_id\": clarification_session.current_question_id, \"readiness_state\": session.readiness_state.value, \"remaining_count\": clarification_session.remaining_count})\n\n return ClarificationStateResult(\n clarification_session=clarification_session,\n current_question=self.build_question_payload(session),\n session=session,\n changed_findings=[changed_finding] if changed_finding else [],\n )\n\n # endregion record_answer\n\n # region summarize_progress [TYPE Function]\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 # endregion summarize_progress\n\n # region _get_latest_clarification_session [TYPE Function]\n # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.\n def _get_latest_clarification_session(self, session: DatasetReviewSession) -> ClarificationSession | None:\n if not session.clarification_sessions:\n return None\n ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)\n return ordered[0]\n\n # endregion _get_latest_clarification_session\n\n # region _find_question [TYPE Function]\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> ClarificationQuestion | None:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # endregion _find_question\n\n\n# #endregion ClarificationEngine\n\n# #endregion ClarificationEngine\n" + "body": "# #region ClarificationEngine [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, clarification, finding, resolution]\n# @BRIEF Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [ClarificationSession]\n# @RELATION DEPENDS_ON -> [ClarificationQuestion]\n# @RELATION DEPENDS_ON -> [ClarificationAnswer]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DISPATCHES -> [ClarificationHelpers]\n# @PRE Target session contains a persisted clarification aggregate in the current ownership scope.\n# @POST Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.\n# @SIDE_EFFECT Persists clarification answers, question/session states, and related readiness/finding changes.\n# @DATA_CONTRACT Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]\n# @INVARIANT Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.\n# @RATIONALE Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.\n# @REJECTED Keeping all clarification logic in one file because it exceeded the fractal limit.\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom datetime import UTC, datetime\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.auth import User\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationAnswer,\n ClarificationQuestion,\n ClarificationSession,\n ClarificationStatus,\n DatasetReviewSession,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n SessionPhase,\n ValidationFinding,\n)\nfrom src.services.dataset_review.clarification_pkg._helpers import (\n build_impact_summary,\n count_remaining_questions,\n count_resolved_questions,\n derive_readiness_state,\n derive_recommended_action,\n normalize_answer_value,\n select_next_open_question,\n upsert_clarification_finding,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\n\n\n# #region ClarificationQuestionPayload [C:2] [TYPE Class]\n# @BRIEF Typed active-question payload returned to the API layer.\n@dataclass\nclass ClarificationQuestionPayload:\n question_id: str\n clarification_session_id: str\n topic_ref: str\n question_text: str\n why_it_matters: str\n current_guess: str | None\n priority: int\n state: QuestionState\n options: list[dict[str, object]] = field(default_factory=list)\n\n\n# #endregion ClarificationQuestionPayload\n\n\n# #region ClarificationStateResult [C:2] [TYPE Class]\n# @BRIEF Clarification state result carrying the current session, active payload, and changed findings.\n@dataclass\nclass ClarificationStateResult:\n clarification_session: ClarificationSession\n current_question: ClarificationQuestionPayload | None\n session: DatasetReviewSession\n changed_findings: list[ValidationFinding] = field(default_factory=list)\n\n\n# #endregion ClarificationStateResult\n\n\n# #region ClarificationAnswerCommand [C:2] [TYPE Class]\n# @BRIEF Typed answer command for clarification state mutation.\n@dataclass\nclass ClarificationAnswerCommand:\n session: DatasetReviewSession\n question_id: str\n answer_kind: AnswerKind\n answer_value: str | None\n user: User\n\n\n# #endregion ClarificationAnswerCommand\n\n\n# #region ClarificationEngine [C:4] [TYPE Class]\n# @BRIEF Provide deterministic one-question-at-a-time clarification selection and answer persistence.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION CALLS -> [ClarificationHelpers]\n# @PRE Repository is bound to the current request transaction scope.\n# @POST Returned clarification state is persistence-backed and aligned with session readiness/recommended action.\n# @SIDE_EFFECT Mutates clarification answers, session flags, and related clarification findings.\nclass ClarificationEngine:\n # region ClarificationEngine_init [TYPE Function]\n # @PURPOSE: Bind repository dependency for clarification persistence operations.\n def __init__(self, repository: DatasetReviewSessionRepository) -> None:\n self.repository = repository\n\n # endregion ClarificationEngine_init\n\n # region build_question_payload [TYPE Function]\n # @PURPOSE: Return the one active highest-priority clarification question payload.\n # @PRE Session contains unresolved clarification state or a resumable clarification session.\n # @POST Returns exactly one active/open question payload or None when no unresolved question remains.\n # @SIDE_EFFECT Normalizes the active-question pointer and clarification status in persistence.\n def build_question_payload(\n self, session: DatasetReviewSession,\n ) -> ClarificationQuestionPayload | None:\n with belief_scope(\"ClarificationEngine.build_question_payload\"):\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.reason(\"No clarification session found\", extra={\"session_id\": session.session_id})\n return None\n\n active_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n active_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n\n if not active_questions:\n clarification_session.current_question_id = None\n clarification_session.status = ClarificationStatus.COMPLETED\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n if session.current_phase == SessionPhase.CLARIFICATION:\n session.current_phase = SessionPhase.REVIEW\n self.repository.db.commit()\n logger.reflect(\"No unresolved clarification question remains\", extra={\"session_id\": session.session_id})\n return None\n\n selected_question = active_questions[0]\n clarification_session.current_question_id = selected_question.question_id\n clarification_session.status = ClarificationStatus.ACTIVE\n session.readiness_state = ReadinessState.CLARIFICATION_ACTIVE\n session.recommended_action = RecommendedAction.ANSWER_NEXT_QUESTION\n session.current_phase = SessionPhase.CLARIFICATION\n\n logger.reason(\"Selected active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": selected_question.question_id, \"priority\": selected_question.priority})\n self.repository.db.commit()\n\n payload = ClarificationQuestionPayload(\n question_id=selected_question.question_id,\n clarification_session_id=selected_question.clarification_session_id,\n topic_ref=selected_question.topic_ref,\n question_text=selected_question.question_text,\n why_it_matters=selected_question.why_it_matters,\n current_guess=selected_question.current_guess,\n priority=selected_question.priority,\n state=selected_question.state,\n options=[\n {\"option_id\": o.option_id, \"question_id\": o.question_id, \"label\": o.label, \"value\": o.value, \"is_recommended\": o.is_recommended, \"display_order\": o.display_order}\n for o in sorted(selected_question.options, key=lambda item: (item.display_order, item.label, item.option_id))\n ],\n )\n logger.reflect(\"Clarification payload built\", extra={\"session_id\": session.session_id, \"question_id\": payload.question_id, \"option_count\": len(payload.options)})\n return payload\n\n # endregion build_question_payload\n\n # region record_answer [TYPE Function]\n # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.\n # @PRE Target question belongs to the session's active clarification session and is still open.\n # @POST Answer row is persisted before current-question pointer advances.\n # @SIDE_EFFECT Inserts answer row, mutates question/session states, updates clarification findings, and commits.\n def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:\n with belief_scope(\"ClarificationEngine.record_answer\"):\n session = command.session\n clarification_session = self._get_latest_clarification_session(session)\n if clarification_session is None:\n logger.explore(\"Cannot record clarification answer because no clarification session exists\", extra={\"session_id\": session.session_id})\n raise ValueError(\"Clarification session not found\")\n\n question = self._find_question(clarification_session, command.question_id)\n if question is None:\n logger.explore(\"Cannot record clarification answer for foreign or missing question\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question not found\")\n\n if question.answer is not None:\n logger.explore(\"Rejected duplicate clarification answer submission\", extra={\"session_id\": session.session_id, \"question_id\": command.question_id})\n raise ValueError(\"Clarification question already answered\")\n\n if clarification_session.current_question_id and clarification_session.current_question_id != question.question_id:\n logger.explore(\"Rejected answer for non-active clarification question\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"current_question_id\": clarification_session.current_question_id})\n raise ValueError(\"Only the active clarification question can be answered\")\n\n normalized_answer_value = normalize_answer_value(command.answer_kind, command.answer_value, question)\n\n logger.reason(\"Persisting clarification answer before state advancement\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"answer_kind\": command.answer_kind.value})\n persisted_answer = ClarificationAnswer(\n question_id=question.question_id,\n answer_kind=command.answer_kind,\n answer_value=normalized_answer_value,\n answered_by_user_id=command.user.id,\n impact_summary=build_impact_summary(question, command.answer_kind, normalized_answer_value),\n )\n self.repository.db.add(persisted_answer)\n self.repository.db.flush()\n\n changed_finding = upsert_clarification_finding(\n session=session, question=question, answer_kind=command.answer_kind,\n answer_value=normalized_answer_value, db_session=self.repository.db,\n )\n\n if command.answer_kind == AnswerKind.SELECTED or command.answer_kind == AnswerKind.CUSTOM:\n question.state = QuestionState.ANSWERED\n elif command.answer_kind == AnswerKind.SKIPPED:\n question.state = QuestionState.SKIPPED\n elif command.answer_kind == AnswerKind.EXPERT_REVIEW:\n question.state = QuestionState.EXPERT_REVIEW\n\n question.updated_at = datetime.now(UTC)\n self.repository.db.flush()\n\n clarification_session.resolved_count = count_resolved_questions(clarification_session)\n clarification_session.remaining_count = count_remaining_questions(clarification_session)\n clarification_session.summary_delta = self.summarize_progress(clarification_session)\n clarification_session.updated_at = datetime.now(UTC)\n\n next_question = select_next_open_question(clarification_session)\n clarification_session.current_question_id = next_question.question_id if next_question else None\n clarification_session.status = ClarificationStatus.ACTIVE if next_question else ClarificationStatus.COMPLETED\n if clarification_session.status == ClarificationStatus.COMPLETED:\n clarification_session.completed_at = datetime.now(UTC)\n\n session.readiness_state = derive_readiness_state(session, clarification_session)\n session.recommended_action = derive_recommended_action(session, clarification_session)\n session.current_phase = SessionPhase.CLARIFICATION if clarification_session.current_question_id else SessionPhase.REVIEW\n self.repository.bump_session_version(session)\n\n self.repository.db.commit()\n self.repository.db.refresh(session)\n\n logger.reflect(\"Clarification answer recorded and session advanced\", extra={\"session_id\": session.session_id, \"question_id\": question.question_id, \"next_question_id\": clarification_session.current_question_id, \"readiness_state\": session.readiness_state.value, \"remaining_count\": clarification_session.remaining_count})\n\n return ClarificationStateResult(\n clarification_session=clarification_session,\n current_question=self.build_question_payload(session),\n session=session,\n changed_findings=[changed_finding] if changed_finding else [],\n )\n\n # endregion record_answer\n\n # region summarize_progress [TYPE Function]\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 # endregion summarize_progress\n\n # region _get_latest_clarification_session [TYPE Function]\n # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.\n def _get_latest_clarification_session(self, session: DatasetReviewSession) -> ClarificationSession | None:\n if not session.clarification_sessions:\n return None\n ordered = sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)\n return ordered[0]\n\n # endregion _get_latest_clarification_session\n\n # region _find_question [TYPE Function]\n # @PURPOSE: Resolve a clarification question from the active clarification aggregate.\n def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> ClarificationQuestion | None:\n for q in clarification_session.questions:\n if q.question_id == question_id:\n return q\n return None\n\n # endregion _find_question\n\n\n# #endregion ClarificationEngine\n\n# #endregion ClarificationEngine\n" }, { "contract_id": "ClarificationQuestionPayload", @@ -45223,7 +46103,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ClarificationHelpers [C:3] [TYPE Module] [SEMANTICS dataset, clarification, question, selection, normalization]\n# @BRIEF Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nimport uuid\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationQuestion,\n ClarificationSession,\n DatasetReviewSession,\n FindingArea,\n FindingSeverity,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n ValidationFinding,\n)\n\n\n# #region select_next_open_question [C:2] [TYPE Function]\n# @BRIEF Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> ClarificationQuestion | None:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# #endregion select_next_open_question\n\n\n# #region count_resolved_questions [C:1] [TYPE Function]\n# @BRIEF 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# #endregion count_resolved_questions\n\n\n# #region count_remaining_questions [C:1] [TYPE Function]\n# @BRIEF 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# #endregion count_remaining_questions\n\n\n# #region normalize_answer_value [C:2] [TYPE Function]\n# @BRIEF Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: str | None,\n question: ClarificationQuestion,\n) -> str | None:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# #endregion normalize_answer_value\n\n\n# #region build_impact_summary [C:1] [TYPE Function]\n# @BRIEF 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: str | None,\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# #endregion build_impact_summary\n\n\n# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n\n\n# #region derive_readiness_state [C:2] [TYPE Function]\n# @BRIEF Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# #endregion derive_readiness_state\n\n\n# #region derive_recommended_action [C:2] [TYPE Function]\n# @BRIEF Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# #endregion derive_recommended_action\n\n\n# #endregion ClarificationHelpers\n" + "body": "# #region ClarificationHelpers [C:3] [TYPE Module] [SEMANTICS dataset, clarification, question, selection, normalization]\n# @BRIEF Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewModels]\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport uuid\n\nfrom src.models.dataset_review import (\n AnswerKind,\n ClarificationQuestion,\n ClarificationSession,\n DatasetReviewSession,\n FindingArea,\n FindingSeverity,\n QuestionState,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n ValidationFinding,\n)\n\n\n# #region select_next_open_question [C:2] [TYPE Function]\n# @BRIEF Select the next unresolved question in deterministic priority order.\ndef select_next_open_question(\n clarification_session: ClarificationSession,\n) -> ClarificationQuestion | None:\n open_questions = [\n q for q in clarification_session.questions if q.state == QuestionState.OPEN\n ]\n if not open_questions:\n return None\n open_questions.sort(key=lambda item: (-int(item.priority), item.created_at, item.question_id))\n return open_questions[0]\n\n\n# #endregion select_next_open_question\n\n\n# #region count_resolved_questions [C:1] [TYPE Function]\n# @BRIEF 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# #endregion count_resolved_questions\n\n\n# #region count_remaining_questions [C:1] [TYPE Function]\n# @BRIEF 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# #endregion count_remaining_questions\n\n\n# #region normalize_answer_value [C:2] [TYPE Function]\n# @BRIEF Validate and normalize answer payload based on answer kind and active question options.\ndef normalize_answer_value(\n answer_kind: AnswerKind,\n answer_value: str | None,\n question: ClarificationQuestion,\n) -> str | None:\n normalized = str(answer_value).strip() if answer_value is not None else None\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM} and not normalized:\n raise ValueError(\"answer_value is required for selected or custom clarification answers\")\n if answer_kind == AnswerKind.SELECTED:\n allowed_values = {option.value for option in question.options}\n if normalized not in allowed_values:\n raise ValueError(\"answer_value must match one of the current clarification options\")\n if answer_kind == AnswerKind.SKIPPED:\n return normalized or \"skipped\"\n if answer_kind == AnswerKind.EXPERT_REVIEW:\n return normalized or \"expert_review\"\n return normalized\n\n\n# #endregion normalize_answer_value\n\n\n# #region build_impact_summary [C:1] [TYPE Function]\n# @BRIEF 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: str | None,\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# #endregion build_impact_summary\n\n\n# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.now(UTC)\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.now(UTC),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n\n\n# #region derive_readiness_state [C:2] [TYPE Function]\n# @BRIEF Recompute readiness after clarification mutation while preserving unresolved visibility semantics.\ndef derive_readiness_state(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> ReadinessState:\n if clarification_session is None:\n return session.readiness_state\n if clarification_session.current_question_id:\n return ReadinessState.CLARIFICATION_ACTIVE\n if clarification_session.remaining_count > 0:\n return ReadinessState.CLARIFICATION_NEEDED\n return ReadinessState.REVIEW_READY\n\n\n# #endregion derive_readiness_state\n\n\n# #region derive_recommended_action [C:2] [TYPE Function]\n# @BRIEF Recompute next-action guidance after clarification mutations.\ndef derive_recommended_action(\n session: DatasetReviewSession,\n clarification_session: ClarificationSession | None,\n) -> RecommendedAction:\n if clarification_session is None:\n return session.recommended_action\n if clarification_session.current_question_id:\n return RecommendedAction.ANSWER_NEXT_QUESTION\n if clarification_session.remaining_count > 0:\n return RecommendedAction.START_CLARIFICATION\n return RecommendedAction.REVIEW_DOCUMENTATION\n\n\n# #endregion derive_recommended_action\n\n\n# #endregion ClarificationHelpers\n" }, { "contract_id": "select_next_open_question", @@ -45338,7 +46218,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.utcnow()\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.utcnow(),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n" + "body": "# #region upsert_clarification_finding [C:3] [TYPE Function]\n# @BRIEF Keep one finding per clarification topic aligned with answer outcome and unresolved visibility rules.\n# @RELATION DEPENDS_ON -> [ValidationFinding]\ndef upsert_clarification_finding(\n session: DatasetReviewSession,\n question: ClarificationQuestion,\n answer_kind: AnswerKind,\n answer_value: str | None,\n db_session,\n) -> ValidationFinding | None:\n caused_by_ref = f\"clarification:{question.question_id}\"\n existing = next(\n (f for f in session.findings if f.area == FindingArea.CLARIFICATION and f.caused_by_ref == caused_by_ref),\n None,\n )\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n resolution_state = ResolutionState.RESOLVED\n resolved_at = datetime.now(UTC)\n message = f\"Clarified '{question.topic_ref}' with answer '{answer_value}'.\"\n elif answer_kind == AnswerKind.SKIPPED:\n resolution_state = ResolutionState.SKIPPED\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' was skipped and still needs review.\"\n else:\n resolution_state = ResolutionState.EXPERT_REVIEW\n resolved_at = None\n message = f\"Clarification for '{question.topic_ref}' requires expert review.\"\n\n if existing is None:\n existing = ValidationFinding(\n finding_id=str(uuid.uuid4()),\n session_id=session.session_id,\n area=FindingArea.CLARIFICATION,\n severity=FindingSeverity.WARNING,\n code=\"CLARIFICATION_PENDING\",\n title=\"Clarification pending\",\n message=message,\n resolution_state=resolution_state,\n resolution_note=None,\n caused_by_ref=caused_by_ref,\n created_at=datetime.now(UTC),\n resolved_at=resolved_at,\n )\n db_session.add(existing)\n session.findings.append(existing)\n else:\n existing.message = message\n existing.resolution_state = resolution_state\n existing.resolved_at = resolved_at\n\n if answer_kind in {AnswerKind.SELECTED, AnswerKind.CUSTOM}:\n existing.code = \"CLARIFICATION_RESOLVED\"\n existing.title = \"Clarification resolved\"\n elif answer_kind == AnswerKind.SKIPPED:\n existing.code = \"CLARIFICATION_SKIPPED\"\n existing.title = \"Clarification skipped\"\n else:\n existing.code = \"CLARIFICATION_EXPERT_REVIEW\"\n existing.title = \"Clarification requires expert review\"\n\n return existing\n\n\n# #endregion upsert_clarification_finding\n" }, { "contract_id": "derive_readiness_state", @@ -45547,7 +46427,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewOrchestrator [C:5] [TYPE Module] [SEMANTICS pydantic, dataset, review, orchestration, session]\n# @BRIEF Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [SupersetCompilationAdapter]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DISPATCHES -> [OrchestratorHelpers]\n# @RELATION DISPATCHES -> [OrchestratorCommands]\n# @PRE session mutations must execute inside a persisted session boundary scoped to one authenticated user.\n# @POST state transitions are persisted atomically and emit observable progress for long-running steps.\n# @SIDE_EFFECT creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.\n# @DATA_CONTRACT Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]\n# @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.\n# @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.\n# @REJECTED Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.\n\nfrom __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any, cast\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n)\nfrom src.models.dataset_review import (\n ApprovalState,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n ImportedFilter,\n LaunchStatus,\n MappingMethod,\n MappingStatus,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n SessionPhase,\n SessionStatus,\n TemplateVariable,\n ValidationFinding,\n VariableKind,\n)\nfrom src.services.dataset_review.event_logger import SessionEventPayload\nfrom src.services.dataset_review.orchestrator_pkg._commands import (\n LaunchDatasetCommand,\n LaunchDatasetResult,\n PreparePreviewCommand,\n PreparePreviewResult,\n StartSessionCommand,\n StartSessionResult,\n)\nfrom src.services.dataset_review.orchestrator_pkg._helpers import (\n build_execution_snapshot,\n build_initial_profile,\n build_launch_blockers,\n build_partial_recovery_findings,\n get_latest_preview,\n parse_dataset_selection,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\nfrom src.services.dataset_review.semantic_resolver import SemanticSourceResolver\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewOrchestrator [C:5] [TYPE Class]\n# @BRIEF Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION CALLS -> [OrchestratorHelpers]\n# @PRE constructor dependencies are valid and tied to the current request/task scope.\n# @POST orchestrator instance can execute session-scoped mutations for one authenticated user.\n# @SIDE_EFFECT downstream operations may persist session/profile/finding state and enqueue background tasks.\n# @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]\n# @INVARIANT session ownership is preserved on every mutation and recovery remains explicit when partial.\nclass DatasetReviewOrchestrator:\n # region DatasetReviewOrchestrator_init [TYPE Function]\n # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.\n # @PRE repository/config_manager are valid collaborators for the current request scope.\n # @POST Instance holds collaborator references used by start/preview/launch orchestration methods.\n def __init__(\n self,\n repository: DatasetReviewSessionRepository,\n config_manager: ConfigManager,\n task_manager: TaskManager | None = None,\n semantic_resolver: SemanticSourceResolver | None = None,\n ) -> None:\n self.repository = repository\n self.config_manager = config_manager\n self.task_manager = task_manager\n self.semantic_resolver = semantic_resolver or SemanticSourceResolver()\n\n # endregion DatasetReviewOrchestrator_init\n\n # region start_session [TYPE Function]\n # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.\n # @RELATION CALLS -> [EXT:method:SupersetContextExtractor.parse_superset_link]\n # @RELATION CALLS -> [EXT:method:TaskManager.create_task]\n # @PRE source input is non-empty and environment is accessible.\n # @POST session exists in persisted storage with intake/recovery state and task linkage when async work is required.\n # @SIDE_EFFECT persists session and may enqueue recovery task.\n # @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]\n # @INVARIANT no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.\n def start_session(self, command: StartSessionCommand) -> StartSessionResult:\n with belief_scope(\"DatasetReviewOrchestrator.start_session\"):\n normalized_source_kind = str(command.source_kind or \"\").strip()\n normalized_source_input = str(command.source_input or \"\").strip()\n normalized_environment_id = str(command.environment_id or \"\").strip()\n\n if not normalized_source_input:\n logger.explore(\"Blocked dataset review session start due to empty source input\")\n raise ValueError(\"source_input must be non-empty\")\n\n if normalized_source_kind not in {\"superset_link\", \"dataset_selection\"}:\n logger.explore(\"Blocked dataset review session start due to unsupported source kind\", extra={\"source_kind\": normalized_source_kind})\n raise ValueError(\"source_kind must be 'superset_link' or 'dataset_selection'\")\n\n environment = self.config_manager.get_environment(normalized_environment_id)\n if environment is None:\n logger.explore(\"Blocked dataset review session start because environment was not found\", extra={\"environment_id\": normalized_environment_id})\n raise ValueError(\"Environment not found\")\n\n logger.reason(\"Starting dataset review session\", extra={\"user_id\": command.user.id, \"environment_id\": normalized_environment_id, \"source_kind\": normalized_source_kind})\n\n parsed_context: SupersetParsedContext | None = None\n findings: list[ValidationFinding] = []\n dataset_ref = normalized_source_input\n dataset_id: int | None = None\n dashboard_id: int | None = None\n readiness_state = ReadinessState.IMPORTING\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n current_phase = SessionPhase.RECOVERY\n\n if normalized_source_kind == \"superset_link\":\n extractor = SupersetContextExtractor(environment)\n parsed_context = extractor.parse_superset_link(normalized_source_input)\n dataset_ref = parsed_context.dataset_ref\n dataset_id = parsed_context.dataset_id\n dashboard_id = parsed_context.dashboard_id\n\n if parsed_context.partial_recovery:\n readiness_state = ReadinessState.RECOVERY_REQUIRED\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n findings.extend(build_partial_recovery_findings(parsed_context))\n else:\n readiness_state = ReadinessState.REVIEW_READY\n else:\n dataset_ref, dataset_id = parse_dataset_selection(normalized_source_input)\n readiness_state = ReadinessState.REVIEW_READY\n current_phase = SessionPhase.REVIEW\n\n session = DatasetReviewSession(\n user_id=command.user.id,\n environment_id=normalized_environment_id,\n source_kind=normalized_source_kind,\n source_input=normalized_source_input,\n dataset_ref=dataset_ref,\n dataset_id=dataset_id,\n dashboard_id=dashboard_id,\n readiness_state=readiness_state,\n recommended_action=recommended_action,\n status=SessionStatus.ACTIVE,\n current_phase=current_phase,\n )\n persisted_session = cast(Any, self.repository.create_session(session))\n\n recovered_filters: list[ImportedFilter] = []\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n if normalized_source_kind == \"superset_link\" and parsed_context is not None:\n recovered_filters, template_variables, execution_mappings, findings = (\n self._build_recovery_bootstrap(\n environment=environment,\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n )\n\n profile = build_initial_profile(\n session_id=persisted_session.session_id,\n parsed_context=parsed_context,\n dataset_ref=dataset_ref,\n )\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"session_started\",\n event_summary=\"Dataset review session shell created\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\n \"source_kind\": persisted_session.source_kind,\n \"dataset_ref\": persisted_session.dataset_ref,\n \"dataset_id\": persisted_session.dataset_id,\n \"dashboard_id\": persisted_session.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n },\n )\n )\n persisted_session = self.repository.save_profile_and_findings(\n persisted_session.session_id,\n command.user.id,\n profile,\n findings,\n )\n if recovered_filters or template_variables or execution_mappings:\n persisted_session = self.repository.save_recovery_state(\n persisted_session.session_id,\n command.user.id,\n recovered_filters,\n template_variables,\n execution_mappings,\n )\n\n active_task_id = self._enqueue_recovery_task(\n command=command,\n session=persisted_session,\n parsed_context=parsed_context,\n )\n if active_task_id:\n persisted_session.active_task_id = active_task_id\n self.repository.bump_session_version(persisted_session)\n self.repository.db.commit()\n self.repository.db.refresh(persisted_session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"recovery_task_linked\",\n event_summary=\"Recovery task linked to dataset review session\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\"task_id\": active_task_id},\n )\n )\n logger.reason(\"Linked recovery task to started dataset review session\", extra={\"session_id\": persisted_session.session_id, \"task_id\": active_task_id})\n\n logger.reflect(\"Dataset review session start completed\", extra={\"session_id\": persisted_session.session_id, \"dataset_ref\": persisted_session.dataset_ref, \"readiness_state\": persisted_session.readiness_state.value, \"active_task_id\": persisted_session.active_task_id, \"finding_count\": len(findings)})\n return StartSessionResult(\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n\n # endregion start_session\n\n # region prepare_launch_preview [TYPE Function]\n # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.\n # @RELATION CALLS -> [SupersetCompilationAdapter.compile_preview]\n # @PRE all required variables have candidate values or explicitly accepted defaults.\n # @POST returns preview artifact in pending, ready, failed, or stale state.\n # @SIDE_EFFECT persists preview attempt and upstream compilation diagnostics.\n # @DATA_CONTRACT Input[PreparePreviewCommand] -> Output[PreparePreviewResult]\n def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:\n with belief_scope(\"DatasetReviewOrchestrator.prepare_launch_preview\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Preview preparation rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Preview requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n preview_blockers = execution_snapshot[\"preview_blockers\"]\n if preview_blockers:\n logger.explore(\"Preview preparation blocked by incomplete execution context\", extra={\"session_id\": session.session_id, \"blocked_reasons\": preview_blockers})\n raise ValueError(\"Preview blocked: \" + \"; \".join(preview_blockers))\n\n adapter = SupersetCompilationAdapter(environment)\n preview = adapter.compile_preview(\n PreviewCompilationPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_fingerprint=execution_snapshot[\"preview_fingerprint\"],\n template_params=execution_snapshot[\"template_params\"],\n effective_filters=execution_snapshot[\"effective_filters\"],\n )\n )\n persisted_preview = self.repository.save_preview(\n session.session_id,\n command.user.id,\n preview,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.PREVIEW\n session.last_activity_at = datetime.utcnow()\n if persisted_preview.preview_status == PreviewStatus.READY:\n launch_blockers = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=persisted_preview)\n if launch_blockers:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n else:\n session.readiness_state = ReadinessState.RUN_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.PARTIALLY_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"preview_generated\",\n event_summary=\"Superset preview generation persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value, \"preview_fingerprint\": persisted_preview.preview_fingerprint},\n )\n )\n\n logger.reflect(\"Superset preview preparation completed\", extra={\"session_id\": session.session_id, \"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value})\n return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[])\n\n # endregion prepare_launch_preview\n\n # region launch_dataset [TYPE Function]\n # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.\n # @RELATION CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]\n # @PRE session is run-ready and compiled preview is current.\n # @POST returns persisted run context with SQL Lab session reference and launch outcome.\n # @SIDE_EFFECT creates SQL Lab execution session and audit snapshot.\n # @DATA_CONTRACT Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]\n # @INVARIANT launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.\n def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult:\n with belief_scope(\"DatasetReviewOrchestrator.launch_dataset\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Launch rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Launch requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n current_preview = get_latest_preview(session)\n launch_blockers_list = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=current_preview)\n if launch_blockers_list:\n logger.explore(\"Launch gate blocked dataset execution\", extra={\"session_id\": session.session_id, \"blocked_reasons\": launch_blockers_list})\n raise ValueError(\"Launch blocked: \" + \"; \".join(launch_blockers_list))\n\n adapter = SupersetCompilationAdapter(environment)\n try:\n sql_lab_session_ref = adapter.create_sql_lab_session(\n SqlLabLaunchPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_id=current_preview.preview_id,\n compiled_sql=str(current_preview.compiled_sql or \"\"),\n template_params=execution_snapshot[\"template_params\"],\n )\n )\n launch_status = LaunchStatus.STARTED\n launch_error = None\n except Exception as exc:\n logger.explore(\"SQL Lab launch failed after passing gates\", extra={\"session_id\": session.session_id, \"error\": str(exc)})\n sql_lab_session_ref = \"unavailable\"\n launch_status = LaunchStatus.FAILED\n launch_error = str(exc)\n\n run_context = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=session.dataset_ref,\n environment_id=session.environment_id,\n preview_id=current_preview.preview_id,\n sql_lab_session_ref=sql_lab_session_ref,\n effective_filters=execution_snapshot[\"effective_filters\"],\n template_params=execution_snapshot[\"template_params\"],\n approved_mapping_ids=execution_snapshot[\"approved_mapping_ids\"],\n semantic_decision_refs=execution_snapshot[\"semantic_decision_refs\"],\n open_warning_refs=execution_snapshot[\"open_warning_refs\"],\n launch_status=launch_status,\n launch_error=launch_error,\n )\n persisted_run_context = self.repository.save_run_context(\n session.session_id,\n command.user.id,\n run_context,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.LAUNCH\n session.last_activity_at = datetime.utcnow()\n if launch_status == LaunchStatus.FAILED:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.RUN_IN_PROGRESS\n session.recommended_action = RecommendedAction.EXPORT_OUTPUTS\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"dataset_launch_requested\",\n event_summary=\"Dataset launch handoff persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value, \"preview_id\": persisted_run_context.preview_id, \"sql_lab_session_ref\": persisted_run_context.sql_lab_session_ref},\n )\n )\n\n logger.reflect(\"Dataset launch orchestration completed with audited run context\", extra={\"session_id\": session.session_id, \"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value})\n return LaunchDatasetResult(session=session, run_context=persisted_run_context, blocked_reasons=[])\n\n # endregion launch_dataset\n\n # region _build_recovery_bootstrap [TYPE Function]\n # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.\n # @PRE session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.\n # @POST Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.\n # @SIDE_EFFECT Performs Superset reads through the extractor and may append warning findings for incomplete recovery.\n def _build_recovery_bootstrap(\n self,\n environment,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext,\n findings: list[ValidationFinding],\n ) -> tuple[list[ImportedFilter], list[TemplateVariable], list[ExecutionMapping], list[ValidationFinding]]:\n session_record = cast(Any, session)\n extractor = SupersetContextExtractor(environment)\n imported_filters_payload = extractor.recover_imported_filters(parsed_context)\n if imported_filters_payload is None:\n imported_filters_payload = []\n imported_filters = [\n ImportedFilter(\n session_id=session_record.session_id,\n filter_name=str(item.get(\"filter_name\") or f\"imported_filter_{index}\"),\n display_name=item.get(\"display_name\"),\n raw_value=item.get(\"raw_value\"),\n raw_value_masked=bool(item.get(\"raw_value_masked\", False)),\n normalized_value=item.get(\"normalized_value\"),\n source=FilterSource(str(item.get(\"source\") or FilterSource.SUPERSET_URL.value)),\n confidence_state=FilterConfidenceState(str(item.get(\"confidence_state\") or FilterConfidenceState.UNRESOLVED.value)),\n requires_confirmation=bool(item.get(\"requires_confirmation\", False)),\n recovery_status=FilterRecoveryStatus(str(item.get(\"recovery_status\") or FilterRecoveryStatus.PARTIAL.value)),\n notes=item.get(\"notes\"),\n )\n for index, item in enumerate(imported_filters_payload)\n ]\n\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n\n if session.dataset_id is not None:\n try:\n dataset_payload = parsed_context.dataset_payload\n if not isinstance(dataset_payload, dict):\n dataset_payload = extractor.client.get_dataset_detail(session_record.dataset_id)\n discovered_variables = extractor.discover_template_variables(dataset_payload)\n template_variables = [\n TemplateVariable(\n session_id=session_record.session_id,\n variable_name=str(item.get(\"variable_name\") or f\"variable_{index}\"),\n expression_source=str(item.get(\"expression_source\") or \"\"),\n variable_kind=VariableKind(str(item.get(\"variable_kind\") or VariableKind.UNKNOWN.value)),\n is_required=bool(item.get(\"is_required\", True)),\n default_value=item.get(\"default_value\"),\n mapping_status=MappingStatus(str(item.get(\"mapping_status\") or MappingStatus.UNMAPPED.value)),\n )\n for index, item in enumerate(discovered_variables)\n ]\n except Exception as exc:\n if \"dataset_template_variable_discovery_failed\" not in parsed_context.unresolved_references:\n parsed_context.unresolved_references.append(\"dataset_template_variable_discovery_failed\")\n if not any(f.caused_by_ref == \"dataset_template_variable_discovery_failed\" for f in findings):\n findings.append(\n ValidationFinding(\n area=FindingArea.TEMPLATE_MAPPING,\n severity=FindingSeverity.WARNING,\n code=\"TEMPLATE_VARIABLE_DISCOVERY_FAILED\",\n title=\"Template variables could not be discovered\",\n message=\"Session remains usable, but dataset template variables still need review.\",\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=\"dataset_template_variable_discovery_failed\",\n )\n )\n logger.explore(\"Template variable discovery failed during session bootstrap\", extra={\"session_id\": session_record.session_id, \"dataset_id\": session_record.dataset_id, \"error\": str(exc)})\n\n filter_lookup = {str(f.filter_name or \"\").strip().lower(): f for f in imported_filters if str(f.filter_name or \"\").strip()}\n for tv in template_variables:\n matched_filter = filter_lookup.get(str(tv.variable_name or \"\").strip().lower())\n if matched_filter is None:\n continue\n requires_explicit_approval = bool(matched_filter.requires_confirmation or matched_filter.recovery_status != FilterRecoveryStatus.RECOVERED)\n execution_mappings.append(\n ExecutionMapping(\n session_id=session_record.session_id,\n filter_id=matched_filter.filter_id,\n variable_id=tv.variable_id,\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=matched_filter.raw_value,\n effective_value=matched_filter.normalized_value if matched_filter.normalized_value is not None else matched_filter.raw_value,\n transformation_note=\"Bootstrapped from Superset recovery context\",\n warning_level=None,\n requires_explicit_approval=requires_explicit_approval,\n approval_state=ApprovalState.PENDING if requires_explicit_approval else ApprovalState.NOT_REQUIRED,\n approved_by_user_id=None,\n approved_at=None,\n )\n )\n\n return imported_filters, template_variables, execution_mappings, findings\n\n # endregion _build_recovery_bootstrap\n\n # region _enqueue_recovery_task [TYPE Function]\n # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.\n # @PRE session is already persisted.\n # @POST returns task identifier when a task could be enqueued, otherwise None.\n # @SIDE_EFFECT may create one background task for progressive recovery.\n def _enqueue_recovery_task(\n self,\n command: StartSessionCommand,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext | None,\n ) -> str | None:\n session_record = cast(Any, session)\n if self.task_manager is None:\n logger.reason(\"Dataset review session started without task manager; continuing synchronously\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_params: dict[str, Any] = {\n \"session_id\": session_record.session_id,\n \"user_id\": command.user.id,\n \"environment_id\": session_record.environment_id,\n \"source_kind\": session_record.source_kind,\n \"source_input\": session_record.source_input,\n \"dataset_ref\": session_record.dataset_ref,\n \"dataset_id\": session_record.dataset_id,\n \"dashboard_id\": session_record.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n }\n\n create_task = getattr(self.task_manager, \"create_task\", None)\n if create_task is None:\n logger.explore(\"Task manager has no create_task method; skipping recovery enqueue\")\n return None\n\n try:\n task_object = create_task(plugin_id=\"dataset-review-recovery\", params=task_params)\n except TypeError:\n logger.explore(\"Recovery task enqueue skipped because task manager create_task contract is incompatible\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_id = getattr(task_object, \"id\", None)\n return str(task_id) if task_id else None\n\n # endregion _enqueue_recovery_task\n\n\n# #endregion DatasetReviewOrchestrator\n\n# #endregion DatasetReviewOrchestrator\n" + "body": "# #region DatasetReviewOrchestrator [C:5] [TYPE Module] [SEMANTICS pydantic, dataset, review, orchestration, session]\n# @BRIEF Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [SupersetCompilationAdapter]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DISPATCHES -> [OrchestratorHelpers]\n# @RELATION DISPATCHES -> [OrchestratorCommands]\n# @PRE session mutations must execute inside a persisted session boundary scoped to one authenticated user.\n# @POST state transitions are persisted atomically and emit observable progress for long-running steps.\n# @SIDE_EFFECT creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.\n# @DATA_CONTRACT Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]\n# @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.\n# @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.\n# @REJECTED Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom typing import Any, cast\n\nfrom src.core.config_manager import ConfigManager\nfrom src.core.logger import belief_scope, logger\nfrom src.core.task_manager import TaskManager\nfrom src.core.utils.superset_compilation_adapter import (\n PreviewCompilationPayload,\n SqlLabLaunchPayload,\n SupersetCompilationAdapter,\n)\nfrom src.core.utils.superset_context_extractor import (\n SupersetContextExtractor,\n SupersetParsedContext,\n)\nfrom src.models.dataset_review import (\n ApprovalState,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n FilterConfidenceState,\n FilterRecoveryStatus,\n FilterSource,\n FindingArea,\n FindingSeverity,\n ImportedFilter,\n LaunchStatus,\n MappingMethod,\n MappingStatus,\n PreviewStatus,\n ReadinessState,\n RecommendedAction,\n ResolutionState,\n SessionPhase,\n SessionStatus,\n TemplateVariable,\n ValidationFinding,\n VariableKind,\n)\nfrom src.services.dataset_review.event_logger import SessionEventPayload\nfrom src.services.dataset_review.orchestrator_pkg._commands import (\n LaunchDatasetCommand,\n LaunchDatasetResult,\n PreparePreviewCommand,\n PreparePreviewResult,\n StartSessionCommand,\n StartSessionResult,\n)\nfrom src.services.dataset_review.orchestrator_pkg._helpers import (\n build_execution_snapshot,\n build_initial_profile,\n build_launch_blockers,\n build_partial_recovery_findings,\n get_latest_preview,\n parse_dataset_selection,\n)\nfrom src.services.dataset_review.repositories.session_repository import (\n DatasetReviewSessionRepository,\n)\nfrom src.services.dataset_review.semantic_resolver import SemanticSourceResolver\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewOrchestrator [C:5] [TYPE Class]\n# @BRIEF Coordinate safe session startup while preserving cross-user isolation and explicit partial recovery.\n# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]\n# @RELATION DEPENDS_ON -> [SupersetContextExtractor]\n# @RELATION DEPENDS_ON -> [TaskManager]\n# @RELATION DEPENDS_ON -> [ConfigManager]\n# @RELATION DEPENDS_ON -> [SemanticSourceResolver]\n# @RELATION CALLS -> [OrchestratorHelpers]\n# @PRE constructor dependencies are valid and tied to the current request/task scope.\n# @POST orchestrator instance can execute session-scoped mutations for one authenticated user.\n# @SIDE_EFFECT downstream operations may persist session/profile/finding state and enqueue background tasks.\n# @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]\n# @INVARIANT session ownership is preserved on every mutation and recovery remains explicit when partial.\nclass DatasetReviewOrchestrator:\n # region DatasetReviewOrchestrator_init [TYPE Function]\n # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.\n # @PRE repository/config_manager are valid collaborators for the current request scope.\n # @POST Instance holds collaborator references used by start/preview/launch orchestration methods.\n def __init__(\n self,\n repository: DatasetReviewSessionRepository,\n config_manager: ConfigManager,\n task_manager: TaskManager | None = None,\n semantic_resolver: SemanticSourceResolver | None = None,\n ) -> None:\n self.repository = repository\n self.config_manager = config_manager\n self.task_manager = task_manager\n self.semantic_resolver = semantic_resolver or SemanticSourceResolver()\n\n # endregion DatasetReviewOrchestrator_init\n\n # region start_session [TYPE Function]\n # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.\n # @RELATION CALLS -> [EXT:method:SupersetContextExtractor.parse_superset_link]\n # @RELATION CALLS -> [EXT:method:TaskManager.create_task]\n # @PRE source input is non-empty and environment is accessible.\n # @POST session exists in persisted storage with intake/recovery state and task linkage when async work is required.\n # @SIDE_EFFECT persists session and may enqueue recovery task.\n # @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]\n # @INVARIANT no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.\n def start_session(self, command: StartSessionCommand) -> StartSessionResult:\n with belief_scope(\"DatasetReviewOrchestrator.start_session\"):\n normalized_source_kind = str(command.source_kind or \"\").strip()\n normalized_source_input = str(command.source_input or \"\").strip()\n normalized_environment_id = str(command.environment_id or \"\").strip()\n\n if not normalized_source_input:\n logger.explore(\"Blocked dataset review session start due to empty source input\")\n raise ValueError(\"source_input must be non-empty\")\n\n if normalized_source_kind not in {\"superset_link\", \"dataset_selection\"}:\n logger.explore(\"Blocked dataset review session start due to unsupported source kind\", extra={\"source_kind\": normalized_source_kind})\n raise ValueError(\"source_kind must be 'superset_link' or 'dataset_selection'\")\n\n environment = self.config_manager.get_environment(normalized_environment_id)\n if environment is None:\n logger.explore(\"Blocked dataset review session start because environment was not found\", extra={\"environment_id\": normalized_environment_id})\n raise ValueError(\"Environment not found\")\n\n logger.reason(\"Starting dataset review session\", extra={\"user_id\": command.user.id, \"environment_id\": normalized_environment_id, \"source_kind\": normalized_source_kind})\n\n parsed_context: SupersetParsedContext | None = None\n findings: list[ValidationFinding] = []\n dataset_ref = normalized_source_input\n dataset_id: int | None = None\n dashboard_id: int | None = None\n readiness_state = ReadinessState.IMPORTING\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n current_phase = SessionPhase.RECOVERY\n\n if normalized_source_kind == \"superset_link\":\n extractor = SupersetContextExtractor(environment)\n parsed_context = extractor.parse_superset_link(normalized_source_input)\n dataset_ref = parsed_context.dataset_ref\n dataset_id = parsed_context.dataset_id\n dashboard_id = parsed_context.dashboard_id\n\n if parsed_context.partial_recovery:\n readiness_state = ReadinessState.RECOVERY_REQUIRED\n recommended_action = RecommendedAction.REVIEW_DOCUMENTATION\n findings.extend(build_partial_recovery_findings(parsed_context))\n else:\n readiness_state = ReadinessState.REVIEW_READY\n else:\n dataset_ref, dataset_id = parse_dataset_selection(normalized_source_input)\n readiness_state = ReadinessState.REVIEW_READY\n current_phase = SessionPhase.REVIEW\n\n session = DatasetReviewSession(\n user_id=command.user.id,\n environment_id=normalized_environment_id,\n source_kind=normalized_source_kind,\n source_input=normalized_source_input,\n dataset_ref=dataset_ref,\n dataset_id=dataset_id,\n dashboard_id=dashboard_id,\n readiness_state=readiness_state,\n recommended_action=recommended_action,\n status=SessionStatus.ACTIVE,\n current_phase=current_phase,\n )\n persisted_session = cast(Any, self.repository.create_session(session))\n\n recovered_filters: list[ImportedFilter] = []\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n if normalized_source_kind == \"superset_link\" and parsed_context is not None:\n recovered_filters, template_variables, execution_mappings, findings = (\n self._build_recovery_bootstrap(\n environment=environment,\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n )\n\n profile = build_initial_profile(\n session_id=persisted_session.session_id,\n parsed_context=parsed_context,\n dataset_ref=dataset_ref,\n )\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"session_started\",\n event_summary=\"Dataset review session shell created\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\n \"source_kind\": persisted_session.source_kind,\n \"dataset_ref\": persisted_session.dataset_ref,\n \"dataset_id\": persisted_session.dataset_id,\n \"dashboard_id\": persisted_session.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n },\n )\n )\n persisted_session = self.repository.save_profile_and_findings(\n persisted_session.session_id,\n command.user.id,\n profile,\n findings,\n )\n if recovered_filters or template_variables or execution_mappings:\n persisted_session = self.repository.save_recovery_state(\n persisted_session.session_id,\n command.user.id,\n recovered_filters,\n template_variables,\n execution_mappings,\n )\n\n active_task_id = self._enqueue_recovery_task(\n command=command,\n session=persisted_session,\n parsed_context=parsed_context,\n )\n if active_task_id:\n persisted_session.active_task_id = active_task_id\n self.repository.bump_session_version(persisted_session)\n self.repository.db.commit()\n self.repository.db.refresh(persisted_session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=persisted_session.session_id,\n actor_user_id=command.user.id,\n event_type=\"recovery_task_linked\",\n event_summary=\"Recovery task linked to dataset review session\",\n current_phase=persisted_session.current_phase.value,\n readiness_state=persisted_session.readiness_state.value,\n event_details={\"task_id\": active_task_id},\n )\n )\n logger.reason(\"Linked recovery task to started dataset review session\", extra={\"session_id\": persisted_session.session_id, \"task_id\": active_task_id})\n\n logger.reflect(\"Dataset review session start completed\", extra={\"session_id\": persisted_session.session_id, \"dataset_ref\": persisted_session.dataset_ref, \"readiness_state\": persisted_session.readiness_state.value, \"active_task_id\": persisted_session.active_task_id, \"finding_count\": len(findings)})\n return StartSessionResult(\n session=persisted_session,\n parsed_context=parsed_context,\n findings=findings,\n )\n\n # endregion start_session\n\n # region prepare_launch_preview [TYPE Function]\n # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.\n # @RELATION CALLS -> [SupersetCompilationAdapter.compile_preview]\n # @PRE all required variables have candidate values or explicitly accepted defaults.\n # @POST returns preview artifact in pending, ready, failed, or stale state.\n # @SIDE_EFFECT persists preview attempt and upstream compilation diagnostics.\n # @DATA_CONTRACT Input[PreparePreviewCommand] -> Output[PreparePreviewResult]\n def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:\n with belief_scope(\"DatasetReviewOrchestrator.prepare_launch_preview\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Preview preparation rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Preview requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n preview_blockers = execution_snapshot[\"preview_blockers\"]\n if preview_blockers:\n logger.explore(\"Preview preparation blocked by incomplete execution context\", extra={\"session_id\": session.session_id, \"blocked_reasons\": preview_blockers})\n raise ValueError(\"Preview blocked: \" + \"; \".join(preview_blockers))\n\n adapter = SupersetCompilationAdapter(environment)\n preview = adapter.compile_preview(\n PreviewCompilationPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_fingerprint=execution_snapshot[\"preview_fingerprint\"],\n template_params=execution_snapshot[\"template_params\"],\n effective_filters=execution_snapshot[\"effective_filters\"],\n )\n )\n persisted_preview = self.repository.save_preview(\n session.session_id,\n command.user.id,\n preview,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.PREVIEW\n session.last_activity_at = datetime.now(UTC)\n if persisted_preview.preview_status == PreviewStatus.READY:\n launch_blockers = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=persisted_preview)\n if launch_blockers:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.APPROVE_MAPPING\n else:\n session.readiness_state = ReadinessState.RUN_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.PARTIALLY_READY\n session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"preview_generated\",\n event_summary=\"Superset preview generation persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value, \"preview_fingerprint\": persisted_preview.preview_fingerprint},\n )\n )\n\n logger.reflect(\"Superset preview preparation completed\", extra={\"session_id\": session.session_id, \"preview_id\": persisted_preview.preview_id, \"preview_status\": persisted_preview.preview_status.value})\n return PreparePreviewResult(session=session, preview=persisted_preview, blocked_reasons=[])\n\n # endregion prepare_launch_preview\n\n # region launch_dataset [TYPE Function]\n # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.\n # @RELATION CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]\n # @PRE session is run-ready and compiled preview is current.\n # @POST returns persisted run context with SQL Lab session reference and launch outcome.\n # @SIDE_EFFECT creates SQL Lab execution session and audit snapshot.\n # @DATA_CONTRACT Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]\n # @INVARIANT launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.\n def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult:\n with belief_scope(\"DatasetReviewOrchestrator.launch_dataset\"):\n session = self.repository.load_session_detail(command.session_id, command.user.id)\n if session is None or session.user_id != command.user.id:\n logger.explore(\"Launch rejected because owned session was not found\", extra={\"session_id\": command.session_id, \"user_id\": command.user.id})\n raise ValueError(\"Session not found\")\n\n if command.expected_version is not None:\n self.repository.require_session_version(session, command.expected_version)\n\n if session.dataset_id is None:\n raise ValueError(\"Launch requires a resolved dataset_id\")\n\n environment = self.config_manager.get_environment(session.environment_id)\n if environment is None:\n raise ValueError(\"Environment not found\")\n\n execution_snapshot = build_execution_snapshot(session)\n current_preview = get_latest_preview(session)\n launch_blockers_list = build_launch_blockers(session=session, execution_snapshot=execution_snapshot, preview=current_preview)\n if launch_blockers_list:\n logger.explore(\"Launch gate blocked dataset execution\", extra={\"session_id\": session.session_id, \"blocked_reasons\": launch_blockers_list})\n raise ValueError(\"Launch blocked: \" + \"; \".join(launch_blockers_list))\n\n adapter = SupersetCompilationAdapter(environment)\n try:\n sql_lab_session_ref = adapter.create_sql_lab_session(\n SqlLabLaunchPayload(\n session_id=session.session_id,\n dataset_id=session.dataset_id,\n preview_id=current_preview.preview_id,\n compiled_sql=str(current_preview.compiled_sql or \"\"),\n template_params=execution_snapshot[\"template_params\"],\n )\n )\n launch_status = LaunchStatus.STARTED\n launch_error = None\n except Exception as exc:\n logger.explore(\"SQL Lab launch failed after passing gates\", extra={\"session_id\": session.session_id, \"error\": str(exc)})\n sql_lab_session_ref = \"unavailable\"\n launch_status = LaunchStatus.FAILED\n launch_error = str(exc)\n\n run_context = DatasetRunContext(\n session_id=session.session_id,\n dataset_ref=session.dataset_ref,\n environment_id=session.environment_id,\n preview_id=current_preview.preview_id,\n sql_lab_session_ref=sql_lab_session_ref,\n effective_filters=execution_snapshot[\"effective_filters\"],\n template_params=execution_snapshot[\"template_params\"],\n approved_mapping_ids=execution_snapshot[\"approved_mapping_ids\"],\n semantic_decision_refs=execution_snapshot[\"semantic_decision_refs\"],\n open_warning_refs=execution_snapshot[\"open_warning_refs\"],\n launch_status=launch_status,\n launch_error=launch_error,\n )\n persisted_run_context = self.repository.save_run_context(\n session.session_id,\n command.user.id,\n run_context,\n expected_version=command.expected_version,\n )\n\n session.current_phase = SessionPhase.LAUNCH\n session.last_activity_at = datetime.now(UTC)\n if launch_status == LaunchStatus.FAILED:\n session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY\n session.recommended_action = RecommendedAction.LAUNCH_DATASET\n else:\n session.readiness_state = ReadinessState.RUN_IN_PROGRESS\n session.recommended_action = RecommendedAction.EXPORT_OUTPUTS\n self.repository.db.commit()\n self.repository.db.refresh(session)\n self.repository.event_logger.log_event(\n SessionEventPayload(\n session_id=session.session_id,\n actor_user_id=command.user.id,\n event_type=\"dataset_launch_requested\",\n event_summary=\"Dataset launch handoff persisted\",\n current_phase=session.current_phase.value,\n readiness_state=session.readiness_state.value,\n event_details={\"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value, \"preview_id\": persisted_run_context.preview_id, \"sql_lab_session_ref\": persisted_run_context.sql_lab_session_ref},\n )\n )\n\n logger.reflect(\"Dataset launch orchestration completed with audited run context\", extra={\"session_id\": session.session_id, \"run_context_id\": persisted_run_context.run_context_id, \"launch_status\": persisted_run_context.launch_status.value})\n return LaunchDatasetResult(session=session, run_context=persisted_run_context, blocked_reasons=[])\n\n # endregion launch_dataset\n\n # region _build_recovery_bootstrap [TYPE Function]\n # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.\n # @PRE session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.\n # @POST Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.\n # @SIDE_EFFECT Performs Superset reads through the extractor and may append warning findings for incomplete recovery.\n def _build_recovery_bootstrap(\n self,\n environment,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext,\n findings: list[ValidationFinding],\n ) -> tuple[list[ImportedFilter], list[TemplateVariable], list[ExecutionMapping], list[ValidationFinding]]:\n session_record = cast(Any, session)\n extractor = SupersetContextExtractor(environment)\n imported_filters_payload = extractor.recover_imported_filters(parsed_context)\n if imported_filters_payload is None:\n imported_filters_payload = []\n imported_filters = [\n ImportedFilter(\n session_id=session_record.session_id,\n filter_name=str(item.get(\"filter_name\") or f\"imported_filter_{index}\"),\n display_name=item.get(\"display_name\"),\n raw_value=item.get(\"raw_value\"),\n raw_value_masked=bool(item.get(\"raw_value_masked\", False)),\n normalized_value=item.get(\"normalized_value\"),\n source=FilterSource(str(item.get(\"source\") or FilterSource.SUPERSET_URL.value)),\n confidence_state=FilterConfidenceState(str(item.get(\"confidence_state\") or FilterConfidenceState.UNRESOLVED.value)),\n requires_confirmation=bool(item.get(\"requires_confirmation\", False)),\n recovery_status=FilterRecoveryStatus(str(item.get(\"recovery_status\") or FilterRecoveryStatus.PARTIAL.value)),\n notes=item.get(\"notes\"),\n )\n for index, item in enumerate(imported_filters_payload)\n ]\n\n template_variables: list[TemplateVariable] = []\n execution_mappings: list[ExecutionMapping] = []\n\n if session.dataset_id is not None:\n try:\n dataset_payload = parsed_context.dataset_payload\n if not isinstance(dataset_payload, dict):\n dataset_payload = extractor.client.get_dataset_detail(session_record.dataset_id)\n discovered_variables = extractor.discover_template_variables(dataset_payload)\n template_variables = [\n TemplateVariable(\n session_id=session_record.session_id,\n variable_name=str(item.get(\"variable_name\") or f\"variable_{index}\"),\n expression_source=str(item.get(\"expression_source\") or \"\"),\n variable_kind=VariableKind(str(item.get(\"variable_kind\") or VariableKind.UNKNOWN.value)),\n is_required=bool(item.get(\"is_required\", True)),\n default_value=item.get(\"default_value\"),\n mapping_status=MappingStatus(str(item.get(\"mapping_status\") or MappingStatus.UNMAPPED.value)),\n )\n for index, item in enumerate(discovered_variables)\n ]\n except Exception as exc:\n if \"dataset_template_variable_discovery_failed\" not in parsed_context.unresolved_references:\n parsed_context.unresolved_references.append(\"dataset_template_variable_discovery_failed\")\n if not any(f.caused_by_ref == \"dataset_template_variable_discovery_failed\" for f in findings):\n findings.append(\n ValidationFinding(\n area=FindingArea.TEMPLATE_MAPPING,\n severity=FindingSeverity.WARNING,\n code=\"TEMPLATE_VARIABLE_DISCOVERY_FAILED\",\n title=\"Template variables could not be discovered\",\n message=\"Session remains usable, but dataset template variables still need review.\",\n resolution_state=ResolutionState.OPEN,\n caused_by_ref=\"dataset_template_variable_discovery_failed\",\n )\n )\n logger.explore(\"Template variable discovery failed during session bootstrap\", extra={\"session_id\": session_record.session_id, \"dataset_id\": session_record.dataset_id, \"error\": str(exc)})\n\n filter_lookup = {str(f.filter_name or \"\").strip().lower(): f for f in imported_filters if str(f.filter_name or \"\").strip()}\n for tv in template_variables:\n matched_filter = filter_lookup.get(str(tv.variable_name or \"\").strip().lower())\n if matched_filter is None:\n continue\n requires_explicit_approval = bool(matched_filter.requires_confirmation or matched_filter.recovery_status != FilterRecoveryStatus.RECOVERED)\n execution_mappings.append(\n ExecutionMapping(\n session_id=session_record.session_id,\n filter_id=matched_filter.filter_id,\n variable_id=tv.variable_id,\n mapping_method=MappingMethod.DIRECT_MATCH,\n raw_input_value=matched_filter.raw_value,\n effective_value=matched_filter.normalized_value if matched_filter.normalized_value is not None else matched_filter.raw_value,\n transformation_note=\"Bootstrapped from Superset recovery context\",\n warning_level=None,\n requires_explicit_approval=requires_explicit_approval,\n approval_state=ApprovalState.PENDING if requires_explicit_approval else ApprovalState.NOT_REQUIRED,\n approved_by_user_id=None,\n approved_at=None,\n )\n )\n\n return imported_filters, template_variables, execution_mappings, findings\n\n # endregion _build_recovery_bootstrap\n\n # region _enqueue_recovery_task [TYPE Function]\n # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.\n # @PRE session is already persisted.\n # @POST returns task identifier when a task could be enqueued, otherwise None.\n # @SIDE_EFFECT may create one background task for progressive recovery.\n def _enqueue_recovery_task(\n self,\n command: StartSessionCommand,\n session: DatasetReviewSession,\n parsed_context: SupersetParsedContext | None,\n ) -> str | None:\n session_record = cast(Any, session)\n if self.task_manager is None:\n logger.reason(\"Dataset review session started without task manager; continuing synchronously\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_params: dict[str, Any] = {\n \"session_id\": session_record.session_id,\n \"user_id\": command.user.id,\n \"environment_id\": session_record.environment_id,\n \"source_kind\": session_record.source_kind,\n \"source_input\": session_record.source_input,\n \"dataset_ref\": session_record.dataset_ref,\n \"dataset_id\": session_record.dataset_id,\n \"dashboard_id\": session_record.dashboard_id,\n \"partial_recovery\": bool(parsed_context and parsed_context.partial_recovery),\n }\n\n create_task = getattr(self.task_manager, \"create_task\", None)\n if create_task is None:\n logger.explore(\"Task manager has no create_task method; skipping recovery enqueue\")\n return None\n\n try:\n task_object = create_task(plugin_id=\"dataset-review-recovery\", params=task_params)\n except TypeError:\n logger.explore(\"Recovery task enqueue skipped because task manager create_task contract is incompatible\", extra={\"session_id\": session_record.session_id})\n return None\n\n task_id = getattr(task_object, \"id\", None)\n return str(task_id) if task_id else None\n\n # endregion _enqueue_recovery_task\n\n\n# #endregion DatasetReviewOrchestrator\n\n# #endregion DatasetReviewOrchestrator\n" }, { "contract_id": "OrchestratorCommands", @@ -46046,7 +46926,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region DatasetReviewSessionRepository [C:5] [TYPE Module] [SEMANTICS dataset, repository, session, aggregate, persistence]\n# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DISPATCHES -> [SessionRepositoryMutations]\n# @PRE repository operations execute within authenticated request or task scope.\n# @POST session aggregate reads are structurally consistent and writes preserve ownership and version semantics.\n# @SIDE_EFFECT reads and writes SQLAlchemy-backed session aggregates.\n# @DATA_CONTRACT Input[SessionMutation] -> Output[PersistedSessionAggregate]\n# @INVARIANT answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.\n# @RATIONALE Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.\n# @REJECTED Keeping all repository operations in one file because it exceeded the fractal limit.\n\nfrom datetime import datetime\nfrom typing import Any, cast\n\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session, joinedload\nfrom sqlalchemy.orm.exc import StaleDataError\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ClarificationQuestion,\n ClarificationSession,\n CompiledPreview,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n SemanticFieldEntry,\n SessionCollaborator,\n TemplateVariable,\n ValidationFinding,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class]\n# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# #endregion DatasetReviewSessionVersionConflictError\n\n\n# #region DatasetReviewSessionRepository [C:4] [TYPE Class]\n# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SessionEventLogger]\n# @PRE constructor receives a live SQLAlchemy session and callers provide authenticated user scope.\n# @POST repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.\n# @SIDE_EFFECT mutates and queries the persistence layer through the injected database session.\nclass DatasetReviewSessionRepository:\n # region init_repo [TYPE Function]\n # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.\n # @PRE db_session is not None\n # @POST Repository instance initialized with valid session\n def __init__(self, db: Session):\n self.db = db\n self.event_logger = SessionEventLogger(db)\n\n # endregion init_repo\n\n # region get_owned_session [TYPE Function]\n # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.\n # @PRE session_id and user_id are non-empty identifiers from the authenticated ownership scope.\n # @POST returns the owned session or raises a deterministic access error.\n def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.get_owned_session\"):\n logger.reason(\"Resolving owner-scoped dataset review session\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)\n .first()\n )\n if not session:\n logger.explore(\"Owner-scoped dataset review session lookup failed\", extra={\"session_id\": session_id, \"user_id\": user_id})\n raise ValueError(\"Session not found or access denied\")\n logger.reflect(\"Owner-scoped dataset review session resolved\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion get_owned_session\n\n # region create_sess [TYPE Function]\n # @PURPOSE: Persist an initial dataset review session shell.\n # @POST session is committed, refreshed, and returned with persisted identifiers.\n def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.create_session\"):\n logger.reason(\"Persisting dataset review session shell\", extra={\"user_id\": session.user_id, \"environment_id\": session.environment_id})\n self.db.add(session)\n self.db.commit()\n self.db.refresh(session)\n logger.reflect(\"Dataset review session shell persisted\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion create_sess\n\n # region require_session_version [TYPE Function]\n # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.\n # @POST returns the same session when versions match; otherwise raises deterministic conflict error.\n def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.require_session_version\"):\n actual_version = int(getattr(session, \"version\", 0) or 0)\n logger.reason(\"Checking optimistic-lock version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n if actual_version != expected_version:\n logger.explore(\"Rejected mutation due to stale session version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)\n logger.reflect(\"Optimistic-lock version accepted\", extra={\"session_id\": session.session_id, \"version\": actual_version})\n return session\n\n # endregion require_session_version\n\n # region bump_session_version [TYPE Function]\n # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.\n # @POST session version increments monotonically.\n def bump_session_version(self, session: DatasetReviewSession) -> int:\n with belief_scope(\"DatasetReviewSessionRepository.bump_session_version\"):\n next_version = int(getattr(session, \"version\", 0) or 0) + 1\n session.version = next_version\n session.last_activity_at = datetime.utcnow()\n logger.reflect(\"Prepared incremented session version\", extra={\"session_id\": session.session_id, \"version\": next_version})\n return next_version\n\n # endregion bump_session_version\n\n # region commit_session_mutation [TYPE Function]\n # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.\n # @POST session mutation is committed with one version increment or a deterministic conflict error is raised.\n def commit_session_mutation(\n self, session: DatasetReviewSession, *, refresh_targets: list[Any] | None = None, expected_version: int | None = None,\n ) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.commit_session_mutation\"):\n observed_version = int(expected_version if expected_version is not None else getattr(session, \"version\", 0) or 0)\n logger.reason(\"Committing session mutation with optimistic lock\", extra={\"session_id\": session.session_id, \"observed_version\": observed_version})\n self.bump_session_version(session)\n try:\n self.db.commit()\n except StaleDataError as exc:\n self.db.rollback()\n actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()\n actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0\n logger.explore(\"Session commit rejected by optimistic lock\", extra={\"session_id\": session.session_id, \"expected_version\": observed_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc\n self.db.refresh(session)\n for target in refresh_targets or []:\n self.db.refresh(target)\n logger.reflect(\"Session mutation committed\", extra={\"session_id\": session.session_id, \"version\": getattr(session, \"version\", None)})\n return session\n\n # endregion commit_session_mutation\n\n # region load_detail [TYPE Function]\n # @PURPOSE: Return the full session aggregate for API and frontend resume flows.\n # @POST Returns SessionDetail with all fields populated or None.\n def load_session_detail(self, session_id: str, user_id: str) -> DatasetReviewSession | None:\n with belief_scope(\"DatasetReviewSessionRepository.load_session_detail\"):\n logger.reason(\"Loading dataset review session detail\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)\n .options(\n joinedload(DatasetReviewSession.profile),\n joinedload(DatasetReviewSession.findings),\n joinedload(DatasetReviewSession.collaborators),\n joinedload(DatasetReviewSession.semantic_sources),\n joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),\n joinedload(DatasetReviewSession.imported_filters),\n joinedload(DatasetReviewSession.template_variables),\n joinedload(DatasetReviewSession.execution_mappings),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),\n joinedload(DatasetReviewSession.previews),\n joinedload(DatasetReviewSession.run_contexts),\n joinedload(DatasetReviewSession.events),\n )\n .filter(DatasetReviewSession.session_id == session_id)\n .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))\n .first()\n )\n logger.reflect(\"Session detail lookup completed\", extra={\"session_id\": session_id, \"found\": bool(session)})\n return session\n\n # endregion load_detail\n\n # region save_profile_and_findings [TYPE Function]\n # @PURPOSE: Persist profile state and replace validation findings for an owned session.\n # @POST stored profile matches the current session and findings are replaced.\n def save_profile_and_findings(\n self, session_id: str, user_id: str, profile: DatasetProfile, findings: list[ValidationFinding], expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save\n return _save(\n self.db, self.event_logger, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,\n )\n\n # endregion save_profile_and_findings\n\n # region save_recovery_state [TYPE Function]\n # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.\n def save_recovery_state(\n self, session_id: str, user_id: str, imported_filters: list[ImportedFilter],\n template_variables: list[TemplateVariable], execution_mappings: list[ExecutionMapping],\n expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, self.load_session_detail,\n session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,\n )\n\n # endregion save_recovery_state\n\n # region save_preview [TYPE Function]\n # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n def save_preview(\n self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: int | None = None,\n ) -> CompiledPreview:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, preview, expected_version,\n )\n\n # endregion save_preview\n\n # region save_run_context [TYPE Function]\n # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n def save_run_context(\n self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: int | None = None,\n ) -> DatasetRunContext:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, run_context, expected_version,\n )\n\n # endregion save_run_context\n\n # region list_user_sess [TYPE Function]\n # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.\n def list_sessions_for_user(self, user_id: str) -> list[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.list_sessions_for_user\"):\n logger.reason(\"Listing dataset review sessions for owner scope\", extra={\"user_id\": user_id})\n sessions = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.user_id == user_id)\n .order_by(DatasetReviewSession.updated_at.desc())\n .all()\n )\n logger.reflect(\"Session list assembled\", extra={\"user_id\": user_id, \"session_count\": len(sessions)})\n return sessions\n\n # endregion list_user_sess\n\n\n# #endregion DatasetReviewSessionRepository\n\n# #endregion DatasetReviewSessionRepository\n" + "body": "# #region DatasetReviewSessionRepository [C:5] [TYPE Module] [SEMANTICS dataset, repository, session, aggregate, persistence]\n# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [DatasetProfile]\n# @RELATION DEPENDS_ON -> [ValidationFinding]\n# @RELATION DEPENDS_ON -> [CompiledPreview]\n# @RELATION DISPATCHES -> [SessionRepositoryMutations]\n# @PRE repository operations execute within authenticated request or task scope.\n# @POST session aggregate reads are structurally consistent and writes preserve ownership and version semantics.\n# @SIDE_EFFECT reads and writes SQLAlchemy-backed session aggregates.\n# @DATA_CONTRACT Input[SessionMutation] -> Output[PersistedSessionAggregate]\n# @INVARIANT answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.\n# @RATIONALE Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.\n# @REJECTED Keeping all repository operations in one file because it exceeded the fractal limit.\n\nfrom datetime import UTC, datetime\nfrom typing import Any, cast\n\nfrom sqlalchemy import or_\nfrom sqlalchemy.orm import Session, joinedload\nfrom sqlalchemy.orm.exc import StaleDataError\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.dataset_review import (\n ClarificationQuestion,\n ClarificationSession,\n CompiledPreview,\n DatasetProfile,\n DatasetReviewSession,\n DatasetRunContext,\n ExecutionMapping,\n ImportedFilter,\n SemanticFieldEntry,\n SessionCollaborator,\n TemplateVariable,\n ValidationFinding,\n)\nfrom src.services.dataset_review.event_logger import SessionEventLogger\n\nlogger = cast(Any, logger)\n\n\n# #region DatasetReviewSessionVersionConflictError [C:2] [TYPE Class]\n# @BRIEF Signal optimistic-lock conflicts for dataset review session mutations.\nclass DatasetReviewSessionVersionConflictError(ValueError):\n def __init__(self, session_id: str, expected_version: int, actual_version: int):\n self.session_id = session_id\n self.expected_version = expected_version\n self.actual_version = actual_version\n super().__init__(\n f\"Session version conflict: expected {expected_version}, actual {actual_version}\"\n )\n\n\n# #endregion DatasetReviewSessionVersionConflictError\n\n\n# #region DatasetReviewSessionRepository [C:4] [TYPE Class]\n# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.\n# @RELATION DEPENDS_ON -> [DatasetReviewSession]\n# @RELATION DEPENDS_ON -> [SessionEventLogger]\n# @PRE constructor receives a live SQLAlchemy session and callers provide authenticated user scope.\n# @POST repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.\n# @SIDE_EFFECT mutates and queries the persistence layer through the injected database session.\nclass DatasetReviewSessionRepository:\n # region init_repo [TYPE Function]\n # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.\n # @PRE db_session is not None\n # @POST Repository instance initialized with valid session\n def __init__(self, db: Session):\n self.db = db\n self.event_logger = SessionEventLogger(db)\n\n # endregion init_repo\n\n # region get_owned_session [TYPE Function]\n # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.\n # @PRE session_id and user_id are non-empty identifiers from the authenticated ownership scope.\n # @POST returns the owned session or raises a deterministic access error.\n def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.get_owned_session\"):\n logger.reason(\"Resolving owner-scoped dataset review session\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.session_id == session_id, DatasetReviewSession.user_id == user_id)\n .first()\n )\n if not session:\n logger.explore(\"Owner-scoped dataset review session lookup failed\", extra={\"session_id\": session_id, \"user_id\": user_id})\n raise ValueError(\"Session not found or access denied\")\n logger.reflect(\"Owner-scoped dataset review session resolved\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion get_owned_session\n\n # region create_sess [TYPE Function]\n # @PURPOSE: Persist an initial dataset review session shell.\n # @POST session is committed, refreshed, and returned with persisted identifiers.\n def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.create_session\"):\n logger.reason(\"Persisting dataset review session shell\", extra={\"user_id\": session.user_id, \"environment_id\": session.environment_id})\n self.db.add(session)\n self.db.commit()\n self.db.refresh(session)\n logger.reflect(\"Dataset review session shell persisted\", extra={\"session_id\": session.session_id})\n return session\n\n # endregion create_sess\n\n # region require_session_version [TYPE Function]\n # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.\n # @POST returns the same session when versions match; otherwise raises deterministic conflict error.\n def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.require_session_version\"):\n actual_version = int(getattr(session, \"version\", 0) or 0)\n logger.reason(\"Checking optimistic-lock version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n if actual_version != expected_version:\n logger.explore(\"Rejected mutation due to stale session version\", extra={\"session_id\": session.session_id, \"expected_version\": expected_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(str(session.session_id), expected_version, actual_version)\n logger.reflect(\"Optimistic-lock version accepted\", extra={\"session_id\": session.session_id, \"version\": actual_version})\n return session\n\n # endregion require_session_version\n\n # region bump_session_version [TYPE Function]\n # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.\n # @POST session version increments monotonically.\n def bump_session_version(self, session: DatasetReviewSession) -> int:\n with belief_scope(\"DatasetReviewSessionRepository.bump_session_version\"):\n next_version = int(getattr(session, \"version\", 0) or 0) + 1\n session.version = next_version\n session.last_activity_at = datetime.now(UTC)\n logger.reflect(\"Prepared incremented session version\", extra={\"session_id\": session.session_id, \"version\": next_version})\n return next_version\n\n # endregion bump_session_version\n\n # region commit_session_mutation [TYPE Function]\n # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.\n # @POST session mutation is committed with one version increment or a deterministic conflict error is raised.\n def commit_session_mutation(\n self, session: DatasetReviewSession, *, refresh_targets: list[Any] | None = None, expected_version: int | None = None,\n ) -> DatasetReviewSession:\n with belief_scope(\"DatasetReviewSessionRepository.commit_session_mutation\"):\n observed_version = int(expected_version if expected_version is not None else getattr(session, \"version\", 0) or 0)\n logger.reason(\"Committing session mutation with optimistic lock\", extra={\"session_id\": session.session_id, \"observed_version\": observed_version})\n self.bump_session_version(session)\n try:\n self.db.commit()\n except StaleDataError as exc:\n self.db.rollback()\n actual_version_row = self.db.query(DatasetReviewSession.version).filter(DatasetReviewSession.session_id == session.session_id).first()\n actual_version = int(actual_version_row[0] or 0) if actual_version_row else 0\n logger.explore(\"Session commit rejected by optimistic lock\", extra={\"session_id\": session.session_id, \"expected_version\": observed_version, \"actual_version\": actual_version})\n raise DatasetReviewSessionVersionConflictError(session.session_id, observed_version, actual_version) from exc\n self.db.refresh(session)\n for target in refresh_targets or []:\n self.db.refresh(target)\n logger.reflect(\"Session mutation committed\", extra={\"session_id\": session.session_id, \"version\": getattr(session, \"version\", None)})\n return session\n\n # endregion commit_session_mutation\n\n # region load_detail [TYPE Function]\n # @PURPOSE: Return the full session aggregate for API and frontend resume flows.\n # @POST Returns SessionDetail with all fields populated or None.\n def load_session_detail(self, session_id: str, user_id: str) -> DatasetReviewSession | None:\n with belief_scope(\"DatasetReviewSessionRepository.load_session_detail\"):\n logger.reason(\"Loading dataset review session detail\", extra={\"session_id\": session_id, \"user_id\": user_id})\n session = (\n self.db.query(DatasetReviewSession)\n .outerjoin(SessionCollaborator, DatasetReviewSession.session_id == SessionCollaborator.session_id)\n .options(\n joinedload(DatasetReviewSession.profile),\n joinedload(DatasetReviewSession.findings),\n joinedload(DatasetReviewSession.collaborators),\n joinedload(DatasetReviewSession.semantic_sources),\n joinedload(DatasetReviewSession.semantic_fields).joinedload(SemanticFieldEntry.candidates),\n joinedload(DatasetReviewSession.imported_filters),\n joinedload(DatasetReviewSession.template_variables),\n joinedload(DatasetReviewSession.execution_mappings),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.options),\n joinedload(DatasetReviewSession.clarification_sessions).joinedload(ClarificationSession.questions).joinedload(ClarificationQuestion.answer),\n joinedload(DatasetReviewSession.previews),\n joinedload(DatasetReviewSession.run_contexts),\n joinedload(DatasetReviewSession.events),\n )\n .filter(DatasetReviewSession.session_id == session_id)\n .filter(or_(DatasetReviewSession.user_id == user_id, SessionCollaborator.user_id == user_id))\n .first()\n )\n logger.reflect(\"Session detail lookup completed\", extra={\"session_id\": session_id, \"found\": bool(session)})\n return session\n\n # endregion load_detail\n\n # region save_profile_and_findings [TYPE Function]\n # @PURPOSE: Persist profile state and replace validation findings for an owned session.\n # @POST stored profile matches the current session and findings are replaced.\n def save_profile_and_findings(\n self, session_id: str, user_id: str, profile: DatasetProfile, findings: list[ValidationFinding], expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_profile_and_findings as _save\n return _save(\n self.db, self.event_logger, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, profile, findings, expected_version,\n )\n\n # endregion save_profile_and_findings\n\n # region save_recovery_state [TYPE Function]\n # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.\n def save_recovery_state(\n self, session_id: str, user_id: str, imported_filters: list[ImportedFilter],\n template_variables: list[TemplateVariable], execution_mappings: list[ExecutionMapping],\n expected_version: int | None = None,\n ) -> DatasetReviewSession:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_recovery_state as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, self.load_session_detail,\n session_id, user_id, imported_filters, template_variables, execution_mappings, expected_version,\n )\n\n # endregion save_recovery_state\n\n # region save_preview [TYPE Function]\n # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.\n def save_preview(\n self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: int | None = None,\n ) -> CompiledPreview:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_preview as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, preview, expected_version,\n )\n\n # endregion save_preview\n\n # region save_run_context [TYPE Function]\n # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.\n def save_run_context(\n self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: int | None = None,\n ) -> DatasetRunContext:\n from src.services.dataset_review.repositories.repository_pkg._mutations import save_run_context as _save\n return _save(\n self.db, self._get_owned_session, self.require_session_version,\n self.commit_session_mutation, session_id, user_id, run_context, expected_version,\n )\n\n # endregion save_run_context\n\n # region list_user_sess [TYPE Function]\n # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.\n def list_sessions_for_user(self, user_id: str) -> list[DatasetReviewSession]:\n with belief_scope(\"DatasetReviewSessionRepository.list_sessions_for_user\"):\n logger.reason(\"Listing dataset review sessions for owner scope\", extra={\"user_id\": user_id})\n sessions = (\n self.db.query(DatasetReviewSession)\n .filter(DatasetReviewSession.user_id == user_id)\n .order_by(DatasetReviewSession.updated_at.desc())\n .all()\n )\n logger.reflect(\"Session list assembled\", extra={\"user_id\": user_id, \"session_count\": len(sessions)})\n return sessions\n\n # endregion list_user_sess\n\n\n# #endregion DatasetReviewSessionRepository\n\n# #endregion DatasetReviewSessionRepository\n" }, { "contract_id": "DatasetReviewSessionVersionConflictError", @@ -46245,7 +47125,7 @@ "contract_type": "Module", "file_path": "backend/src/services/git/_base.py", "start_line": 1, - "end_line": 395, + "end_line": 391, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -46283,14 +47163,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]\n# @LAYER Infrastructure\n# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RATIONALE Extracted 'git_repos' default into DEFAULT_GIT_REPOS_PATH constant. Fixed typo 'repositorys' → 'repositories' with breaking change warning for existing installations.\n\nfrom contextlib import contextmanager\nimport os\nfrom pathlib import Path\nimport re\nimport shutil\nimport threading\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import InvalidGitRepositoryError, NoSuchPathError\nimport httpx\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.config import AppConfigRecord\nfrom src.models.git import GitRepository\n\n\n# #region GitServiceBase [C:4] [TYPE Class]\n# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, identity, concurrent locking, and shared HTTP client.\n# @PRE base_path is a valid string path.\n# @POST GitService is initialized; base_path directory exists; shared AsyncClient ready; lock registry empty.\n# @SIDE_EFFECT Creates HTTP connection pool (max_keepalive=20, max_connections=100).\nclass GitServiceBase:\n # region GitService_init [C:4] [TYPE Function]\n # @PURPOSE Initializes the GitService with a base path, concurrent access locks, and shared HTTP client.\n # @PRE base_path is a valid string path.\n # @POST GitService is initialized; base_path directory exists; _lock_registry, _http_client ready.\n def __init__(self, base_path: str = \"git_repos\"):\n with belief_scope(\"GitService.__init__\"):\n backend_root = Path(__file__).parents[3]\n self.legacy_base_path = str((backend_root / \"git_repos\").resolve())\n self._uses_default_base_path = base_path == \"git_repos\"\n self.base_path = self._resolve_base_path(base_path)\n self._ensure_base_path_exists()\n\n # Fix 3: Per-dashboard reentrant lock registry for concurrent access protection\n self._lock_registry: dict[int, threading.RLock] = {}\n self._reg_lock = threading.Lock()\n\n # Fix: close() race condition protection\n self._closed = False\n self._close_lock = threading.Lock()\n\n # Fix 5: Shared httpx.AsyncClient with connection pooling\n self._http_client = httpx.AsyncClient(\n timeout=30.0,\n limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),\n )\n # endregion GitService_init\n\n # region _get_lock [C:2] [TYPE Function] [SEMANTICS lock,concurrency]\n # @BRIEF Get or create a per-dashboard reentrant lock for thread-safe GitPython access.\n # @PRE dashboard_id is a valid integer.\n # @POST Returns a threading.RLock unique to the given dashboard_id.\n def _get_lock(self, dashboard_id: int) -> threading.RLock:\n with self._reg_lock:\n if dashboard_id not in self._lock_registry:\n self._lock_registry[dashboard_id] = threading.RLock()\n return self._lock_registry[dashboard_id]\n # endregion _get_lock\n\n # region _locked [C:2] [TYPE Function] [SEMANTICS lock,context,concurrency]\n # @BRIEF Context manager that acquires the per-dashboard reentrant lock for the duration of the block.\n # @PRE dashboard_id is a valid integer.\n # @POST Lock is acquired on enter, released on exit.\n @contextmanager\n def _locked(self, dashboard_id: int):\n if self._closed:\n raise RuntimeError(\"GitService is closed\")\n lock = self._get_lock(dashboard_id)\n lock.acquire()\n try:\n yield\n finally:\n lock.release()\n # endregion _locked\n\n # region _clone_with_auth [C:2] [TYPE Function] [SEMANTICS git,clone,auth,security]\n # @BRIEF Clone repository with PAT and immediately strip credentials from origin remote URL.\n # @PRE remote_url is a valid Git URL; pat is provided when required; repo_path is writable.\n # @POST Repo cloned at repo_path; origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository to local filesystem.\n def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:\n auth_url = remote_url\n if pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{quote(pat, safe='')}@{rest}\"\n repo = Repo.clone_from(auth_url, repo_path)\n # Strip PAT from origin URL to prevent credential leakage in .git/config, logs, ps aux\n repo.git.remote(\"set-url\", \"origin\", remote_url)\n return repo\n # endregion _clone_with_auth\n\n # region _ensure_base_path_exists [TYPE Function]\n # @PURPOSE Ensure the repositories root directory exists and is a directory.\n # @PRE self.base_path is resolved to filesystem path.\n # @POST self.base_path exists as directory or raises ValueError.\n def _ensure_base_path_exists(self) -> None:\n base = Path(self.base_path)\n if base.exists() and not base.is_dir():\n raise ValueError(f\"Git repositories base path is not a directory: {self.base_path}\")\n try:\n base.mkdir(parents=True, exist_ok=True)\n except (PermissionError, OSError) as e:\n logger.warning(\n f\"[_ensure_base_path_exists][Coherence:Failed] Cannot create Git repositories base path: {self.base_path}. Error: {e}\"\n )\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # endregion _ensure_base_path_exists\n\n # region _resolve_base_path [TYPE Function]\n # @PURPOSE Resolve base repository directory from explicit argument or global storage settings.\n # @PRE base_path is a string path.\n # @POST Returns absolute path for Git repositories root.\n # @RETURN str\n def _resolve_base_path(self, base_path: str) -> str:\n backend_root = Path(__file__).parents[3]\n fallback_path = str((backend_root / base_path).resolve())\n if base_path != \"git_repos\":\n return fallback_path\n try:\n session = SessionLocal()\n try:\n config_row = session.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n finally:\n session.close()\n payload = (config_row.payload if config_row and config_row.payload else {}) if config_row else {}\n storage_cfg = payload.get(\"settings\", {}).get(\"storage\", {}) if isinstance(payload, dict) else {}\n root_path = str(storage_cfg.get(\"root_path\", \"\")).strip()\n repo_path = str(storage_cfg.get(\"repo_path\", \"\")).strip()\n if not root_path:\n return fallback_path\n project_root = Path(__file__).parents[4]\n root = Path(root_path)\n if not root.is_absolute():\n root = (project_root / root).resolve()\n repo_root = Path(repo_path) if repo_path else Path(\"repositories\")\n if repo_root.is_absolute():\n return str(repo_root.resolve())\n return str((root / repo_root).resolve())\n except Exception as e:\n logger.warning(f\"[_resolve_base_path][Coherence:Failed] Falling back to default path: {e}\")\n return fallback_path\n # endregion _resolve_base_path\n\n # region _normalize_repo_key [TYPE Function]\n # @PURPOSE Convert user/dashboard-provided key to safe filesystem directory name.\n # @PRE repo_key can be None/empty.\n # @POST Returns normalized non-empty key.\n # @RETURN str\n def _normalize_repo_key(self, repo_key: str | None) -> str:\n raw_key = str(repo_key or \"\").strip().lower()\n normalized = re.sub(r\"[^a-z0-9._-]+\", \"-\", raw_key).strip(\"._-\")\n return normalized or \"dashboard\"\n # endregion _normalize_repo_key\n\n # region _update_repo_local_path [TYPE Function]\n # @PURPOSE Persist repository local_path in GitRepository table when record exists.\n # @PRE dashboard_id is valid integer.\n # @POST local_path is updated for existing record.\n def _update_repo_local_path(self, dashboard_id: int, local_path: str) -> None:\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n db_repo.local_path = local_path\n session.commit()\n finally:\n session.close()\n except Exception as e:\n logger.warning(f\"[_update_repo_local_path][Coherence:Failed] {e}\")\n # endregion _update_repo_local_path\n\n # region _migrate_repo_directory [TYPE Function]\n # @PURPOSE Move legacy repository directory to target path and sync DB metadata.\n # @PRE source_path exists.\n # @POST Repository content available at target_path.\n # @RETURN str\n def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:\n source_abs = os.path.abspath(source_path)\n target_abs = os.path.abspath(target_path)\n if source_abs == target_abs:\n return source_abs\n if os.path.exists(target_abs):\n logger.reason(f\"Target already exists, keeping source path: {target_abs}\", extra={\"src\": \"_migrate_repo_directory\"})\n return source_abs\n Path(target_abs).parent.mkdir(parents=True, exist_ok=True)\n try:\n os.replace(source_abs, target_abs)\n except OSError:\n shutil.move(source_abs, target_abs)\n self._update_repo_local_path(dashboard_id, target_abs)\n logger.info(\n f\"[_migrate_repo_directory][Coherence:OK] Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\"\n )\n return target_abs\n # endregion _migrate_repo_directory\n\n # region _get_repo_path [TYPE Function]\n # @PURPOSE Resolves the local filesystem path for a dashboard's repository.\n # @PARAM dashboard_id (int)\n # @PARAM repo_key (Optional[str]) - Slug-like key used when DB local_path is absent.\n # @PRE dashboard_id is an integer.\n # @POST Returns DB-local_path when present, otherwise base_path/.\n # @RETURN str\n def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:\n with belief_scope(\"GitService._get_repo_path\"):\n if dashboard_id is None:\n raise ValueError(\"dashboard_id cannot be None\")\n self._ensure_base_path_exists()\n fallback_key = repo_key if repo_key is not None else str(dashboard_id)\n normalized_key = self._normalize_repo_key(fallback_key)\n target_path = os.path.join(self.base_path, normalized_key)\n if not self._uses_default_base_path:\n return target_path\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n finally:\n session.close()\n if db_repo and db_repo.local_path:\n db_path = os.path.abspath(db_repo.local_path)\n if os.path.exists(db_path):\n if (\n os.path.abspath(self.base_path) != os.path.abspath(self.legacy_base_path)\n and db_path.startswith(os.path.abspath(self.legacy_base_path) + os.sep)\n ):\n return self._migrate_repo_directory(dashboard_id, db_path, target_path)\n return db_path\n except Exception as e:\n logger.warning(f\"[_get_repo_path][Coherence:Failed] Could not resolve local_path from DB: {e}\")\n legacy_id_path = os.path.join(self.legacy_base_path, str(dashboard_id))\n if os.path.exists(legacy_id_path) and not os.path.exists(target_path):\n return self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)\n if os.path.exists(target_path):\n self._update_repo_local_path(dashboard_id, target_path)\n return target_path\n # endregion _get_repo_path\n\n # region init_repo [C:4] [TYPE Function] [SEMANTICS git,clone,init,lock]\n # @PURPOSE Initialize or clone a repository for a dashboard with concurrent access protection.\n # @PARAM dashboard_id (int), remote_url (str), pat (str), repo_key (Optional[str]).\n # @PRE dashboard_id is int, remote_url is valid Git URL, pat is provided.\n # @POST Repository is cloned or opened at the local path. Origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository; modifies local filesystem; protected by per-dashboard lock.\n # @RETURN Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None, default_branch: str | None = None) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.init_repo\"):\n self._ensure_base_path_exists()\n repo_path = self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))\n Path(repo_path).parent.mkdir(parents=True, exist_ok=True)\n if os.path.exists(repo_path):\n logger.reason(f\"Opening existing repo at {repo_path}\", extra={\"src\": \"init_repo\"})\n try:\n repo = Repo(repo_path)\n except (InvalidGitRepositoryError, NoSuchPathError):\n logger.reason(f\"Existing path is not a Git repository, recreating: {repo_path}\", extra={\"src\": \"init_repo\"})\n stale_path = Path(repo_path)\n if stale_path.exists():\n shutil.rmtree(stale_path, ignore_errors=True)\n if stale_path.exists():\n try:\n stale_path.unlink()\n except Exception:\n pass\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n logger.reason(f\"Cloning {remote_url} to {repo_path}\", extra={\"src\": \"init_repo\"})\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # endregion init_repo\n\n # region delete_repo [C:4] [TYPE Function] [SEMANTICS git,delete,lock]\n # @PURPOSE Remove local repository and DB binding for a dashboard with concurrent access protection.\n # @PRE dashboard_id is a valid integer.\n # @POST Local path is deleted when present and GitRepository row is removed.\n # @SIDE_EFFECT Deletes filesystem directory and DB record; protected by per-dashboard lock.\n def delete_repo(self, dashboard_id: int) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.delete_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n removed_files = False\n if os.path.exists(repo_path):\n if os.path.isdir(repo_path):\n shutil.rmtree(repo_path)\n else:\n os.remove(repo_path)\n removed_files = True\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n session.delete(db_repo)\n session.commit()\n return\n if removed_files:\n return\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_id} not found\",\n )\n except HTTPException:\n session.rollback()\n raise\n except Exception as e:\n session.rollback()\n logger.error(f\"[delete_repo][Coherence:Failed] Failed to delete repository for dashboard {dashboard_id}: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {e!s}\")\n finally:\n session.close()\n # endregion delete_repo\n\n # region get_repo [C:4] [TYPE Function] [SEMANTICS git,open,lock]\n # @PURPOSE Get Repo object for a dashboard with concurrent access protection.\n # @PRE Repository must exist on disk for the given dashboard_id.\n # @POST Returns a GitPython Repo instance for the dashboard.\n # @RETURN Repo\n def get_repo(self, dashboard_id: int) -> Repo:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.error(f\"[EXT:method:get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist\")\n raise HTTPException(status_code=404, detail=f\"Repository for dashboard {dashboard_id} not found\")\n try:\n return Repo(repo_path)\n except Exception as e:\n logger.error(f\"[EXT:method:get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}\")\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # endregion get_repo\n\n # region configure_identity [C:4] [TYPE Function] [SEMANTICS git,identity,lock]\n # @PURPOSE Configure repository-local Git committer identity with concurrent access protection.\n # @PRE dashboard_id repository exists; git_username/git_email may be empty.\n # @POST Repository config has user.name and user.email when both identity values are provided.\n # @SIDE_EFFECT Writes to repository git config; protected by per-dashboard lock.\n def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.configure_identity\"):\n normalized_username = str(git_username or \"\").strip()\n normalized_email = str(git_email or \"\").strip()\n if not normalized_username or not normalized_email:\n return\n repo = self.get_repo(dashboard_id)\n try:\n with repo.config_writer(config_level=\"repository\") as config_writer:\n config_writer.set_value(\"user\", \"name\", normalized_username)\n config_writer.set_value(\"user\", \"email\", normalized_email)\n logger.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\", extra={\"src\": \"configure_identity\"})\n except Exception as e:\n logger.error(f\"[configure_identity][Coherence:Failed] Failed to configure git identity: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {e!s}\")\n # endregion configure_identity\n\n # region close [C:2] [TYPE Function] [SEMANTICS http,cleanup,shutdown]\n # @BRIEF Gracefully close the shared HTTP client (connection pool).\n # @PRE _http_client is initialized.\n # @POST HTTP connections closed.\n async def close(self):\n with self._close_lock:\n if self._closed:\n return\n self._closed = True\n await self._http_client.aclose()\n # endregion close\n# #endregion GitServiceBase\n# #endregion GitServiceBase\n" + "body": "# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]\n# @LAYER Infrastructure\n# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RELATION DEPENDS_ON -> [GitRepository]\n# @RATIONALE Extracted 'git_repos' default into DEFAULT_GIT_REPOS_PATH constant. Fixed typo 'repositorys' → 'repositories' with breaking change warning for existing installations.\n\nfrom contextlib import contextmanager\nimport os\nfrom pathlib import Path\nimport re\nimport shutil\nimport threading\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import InvalidGitRepositoryError, NoSuchPathError\nimport httpx\n\nfrom src.core.database import SessionLocal\nfrom src.core.logger import belief_scope, logger\nfrom src.models.config import AppConfigRecord\nfrom src.models.git import GitRepository\n\n\n# #region GitServiceBase [C:4] [TYPE Class]\n# @BRIEF Base class for GitService providing initialization, path resolution, repository lifecycle, identity, concurrent locking, and shared HTTP client.\n# @PRE base_path is a valid string path.\n# @POST GitService is initialized; base_path directory exists; shared AsyncClient ready; lock registry empty.\n# @SIDE_EFFECT Creates HTTP connection pool (max_keepalive=20, max_connections=100).\nclass GitServiceBase:\n # region GitService_init [C:4] [TYPE Function]\n # @PURPOSE Initializes the GitService with a base path, concurrent access locks, and shared HTTP client.\n # @PRE base_path is a valid string path.\n # @POST GitService is initialized; base_path directory exists; _lock_registry, _http_client ready.\n def __init__(self, base_path: str = \"git_repos\"):\n with belief_scope(\"GitService.__init__\"):\n backend_root = Path(__file__).parents[3]\n self.legacy_base_path = str((backend_root / \"git_repos\").resolve())\n self._uses_default_base_path = base_path == \"git_repos\"\n self.base_path = self._resolve_base_path(base_path)\n self._ensure_base_path_exists()\n\n # Fix 3: Per-dashboard reentrant lock registry for concurrent access protection\n self._lock_registry: dict[int, threading.RLock] = {}\n self._reg_lock = threading.Lock()\n\n # Fix: close() race condition protection\n self._closed = False\n self._close_lock = threading.Lock()\n\n # Fix 5: Shared httpx.AsyncClient with connection pooling\n self._http_client = httpx.AsyncClient(\n timeout=30.0,\n limits=httpx.Limits(max_keepalive_connections=20, max_connections=100),\n )\n # endregion GitService_init\n\n # region _get_lock [C:2] [TYPE Function] [SEMANTICS lock,concurrency]\n # @BRIEF Get or create a per-dashboard reentrant lock for thread-safe GitPython access.\n # @PRE dashboard_id is a valid integer.\n # @POST Returns a threading.RLock unique to the given dashboard_id.\n def _get_lock(self, dashboard_id: int) -> threading.RLock:\n with self._reg_lock:\n if dashboard_id not in self._lock_registry:\n self._lock_registry[dashboard_id] = threading.RLock()\n return self._lock_registry[dashboard_id]\n # endregion _get_lock\n\n # region _locked [C:2] [TYPE Function] [SEMANTICS lock,context,concurrency]\n # @BRIEF Context manager that acquires the per-dashboard reentrant lock for the duration of the block.\n # @PRE dashboard_id is a valid integer.\n # @POST Lock is acquired on enter, released on exit.\n @contextmanager\n def _locked(self, dashboard_id: int):\n if self._closed:\n raise RuntimeError(\"GitService is closed\")\n lock = self._get_lock(dashboard_id)\n lock.acquire()\n try:\n yield\n finally:\n lock.release()\n # endregion _locked\n\n # region _clone_with_auth [C:2] [TYPE Function] [SEMANTICS git,clone,auth,security]\n # @BRIEF Clone repository with PAT and immediately strip credentials from origin remote URL.\n # @PRE remote_url is a valid Git URL; pat is provided when required; repo_path is writable.\n # @POST Repo cloned at repo_path; origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository to local filesystem.\n def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:\n auth_url = remote_url\n if pat and \"://\" in remote_url:\n proto, rest = remote_url.split(\"://\", 1)\n auth_url = f\"{proto}://oauth2:{quote(pat, safe='')}@{rest}\"\n repo = Repo.clone_from(auth_url, repo_path)\n # Strip PAT from origin URL to prevent credential leakage in .git/config, logs, ps aux\n repo.git.remote(\"set-url\", \"origin\", remote_url)\n return repo\n # endregion _clone_with_auth\n\n # region _ensure_base_path_exists [TYPE Function]\n # @PURPOSE Ensure the repositories root directory exists and is a directory.\n # @PRE self.base_path is resolved to filesystem path.\n # @POST self.base_path exists as directory or raises ValueError.\n def _ensure_base_path_exists(self) -> None:\n base = Path(self.base_path)\n if base.exists() and not base.is_dir():\n raise ValueError(f\"Git repositories base path is not a directory: {self.base_path}\")\n try:\n base.mkdir(parents=True, exist_ok=True)\n except (PermissionError, OSError) as e:\n logger.warning(\n f\"[_ensure_base_path_exists][Coherence:Failed] Cannot create Git repositories base path: {self.base_path}. Error: {e}\"\n )\n raise ValueError(f\"Cannot create Git repositories base path: {self.base_path}. {e}\")\n # endregion _ensure_base_path_exists\n\n # region _resolve_base_path [TYPE Function]\n # @PURPOSE Resolve base repository directory from explicit argument or global storage settings.\n # @PRE base_path is a string path.\n # @POST Returns absolute path for Git repositories root.\n # @RETURN str\n def _resolve_base_path(self, base_path: str) -> str:\n backend_root = Path(__file__).parents[3]\n fallback_path = str((backend_root / base_path).resolve())\n if base_path != \"git_repos\":\n return fallback_path\n try:\n session = SessionLocal()\n try:\n config_row = session.query(AppConfigRecord).filter(AppConfigRecord.id == \"global\").first()\n finally:\n session.close()\n payload = (config_row.payload if config_row and config_row.payload else {}) if config_row else {}\n storage_cfg = payload.get(\"settings\", {}).get(\"storage\", {}) if isinstance(payload, dict) else {}\n root_path = str(storage_cfg.get(\"root_path\", \"\")).strip()\n repo_path = str(storage_cfg.get(\"repo_path\", \"\")).strip()\n if not root_path:\n return fallback_path\n project_root = Path(__file__).parents[4]\n root = Path(root_path)\n if not root.is_absolute():\n root = (project_root / root).resolve()\n repo_root = Path(repo_path) if repo_path else Path(\"repositories\")\n if repo_root.is_absolute():\n return str(repo_root.resolve())\n return str((root / repo_root).resolve())\n except Exception as e:\n logger.warning(f\"[_resolve_base_path][Coherence:Failed] Falling back to default path: {e}\")\n return fallback_path\n # endregion _resolve_base_path\n\n # region _normalize_repo_key [TYPE Function]\n # @PURPOSE Convert user/dashboard-provided key to safe filesystem directory name.\n # @PRE repo_key can be None/empty.\n # @POST Returns normalized non-empty key.\n # @RETURN str\n def _normalize_repo_key(self, repo_key: str | None) -> str:\n raw_key = str(repo_key or \"\").strip().lower()\n normalized = re.sub(r\"[^a-z0-9._-]+\", \"-\", raw_key).strip(\"._-\")\n return normalized or \"dashboard\"\n # endregion _normalize_repo_key\n\n # region _update_repo_local_path [TYPE Function]\n # @PURPOSE Persist repository local_path in GitRepository table when record exists.\n # @PRE dashboard_id is valid integer.\n # @POST local_path is updated for existing record.\n def _update_repo_local_path(self, dashboard_id: int, local_path: str) -> None:\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n db_repo.local_path = local_path\n session.commit()\n finally:\n session.close()\n except Exception as e:\n logger.warning(f\"[_update_repo_local_path][Coherence:Failed] {e}\")\n # endregion _update_repo_local_path\n\n # region _migrate_repo_directory [TYPE Function]\n # @PURPOSE Move legacy repository directory to target path and sync DB metadata.\n # @PRE source_path exists.\n # @POST Repository content available at target_path.\n # @RETURN str\n def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:\n source_abs = os.path.abspath(source_path)\n target_abs = os.path.abspath(target_path)\n if source_abs == target_abs:\n return source_abs\n if os.path.exists(target_abs):\n logger.reason(f\"Target already exists, keeping source path: {target_abs}\", extra={\"src\": \"_migrate_repo_directory\"})\n return source_abs\n Path(target_abs).parent.mkdir(parents=True, exist_ok=True)\n try:\n os.replace(source_abs, target_abs)\n except OSError:\n shutil.move(source_abs, target_abs)\n self._update_repo_local_path(dashboard_id, target_abs)\n logger.info(\n f\"[_migrate_repo_directory][Coherence:OK] Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}\"\n )\n return target_abs\n # endregion _migrate_repo_directory\n\n # region _get_repo_path [TYPE Function]\n # @PURPOSE Resolves the local filesystem path for a dashboard's repository.\n # @PARAM dashboard_id (int)\n # @PARAM repo_key (Optional[str]) - Slug-like key used when DB local_path is absent.\n # @PRE dashboard_id is an integer.\n # @POST Returns DB-local_path when present, otherwise base_path/.\n # @RETURN str\n def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:\n with belief_scope(\"GitService._get_repo_path\"):\n if dashboard_id is None:\n raise ValueError(\"dashboard_id cannot be None\")\n self._ensure_base_path_exists()\n fallback_key = repo_key if repo_key is not None else str(dashboard_id)\n normalized_key = self._normalize_repo_key(fallback_key)\n target_path = os.path.join(self.base_path, normalized_key)\n if not self._uses_default_base_path:\n return target_path\n try:\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n finally:\n session.close()\n if db_repo and db_repo.local_path:\n db_path = os.path.abspath(db_repo.local_path)\n if os.path.exists(db_path):\n if (\n os.path.abspath(self.base_path) != os.path.abspath(self.legacy_base_path)\n and db_path.startswith(os.path.abspath(self.legacy_base_path) + os.sep)\n ):\n return self._migrate_repo_directory(dashboard_id, db_path, target_path)\n return db_path\n except Exception as e:\n logger.warning(f\"[_get_repo_path][Coherence:Failed] Could not resolve local_path from DB: {e}\")\n legacy_id_path = os.path.join(self.legacy_base_path, str(dashboard_id))\n if os.path.exists(legacy_id_path) and not os.path.exists(target_path):\n return self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)\n if os.path.exists(target_path):\n self._update_repo_local_path(dashboard_id, target_path)\n return target_path\n # endregion _get_repo_path\n\n # region init_repo [C:4] [TYPE Function] [SEMANTICS git,clone,init,lock]\n # @PURPOSE Initialize or clone a repository for a dashboard with concurrent access protection.\n # @PARAM dashboard_id (int), remote_url (str), pat (str), repo_key (Optional[str]).\n # @PRE dashboard_id is int, remote_url is valid Git URL, pat is provided.\n # @POST Repository is cloned or opened at the local path. Origin remote URL has no embedded PAT.\n # @SIDE_EFFECT Clones remote repository; modifies local filesystem; protected by per-dashboard lock.\n # @RETURN Repo\n def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None, default_branch: str | None = None) -> Repo:\n with self._locked(dashboard_id), belief_scope(\"GitService.init_repo\"):\n self._ensure_base_path_exists()\n repo_path = self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))\n Path(repo_path).parent.mkdir(parents=True, exist_ok=True)\n if os.path.exists(repo_path):\n logger.reason(f\"Opening existing repo at {repo_path}\", extra={\"src\": \"init_repo\"})\n try:\n repo = Repo(repo_path)\n except (InvalidGitRepositoryError, NoSuchPathError):\n logger.reason(f\"Existing path is not a Git repository, recreating: {repo_path}\", extra={\"src\": \"init_repo\"})\n stale_path = Path(repo_path)\n if stale_path.exists():\n shutil.rmtree(stale_path, ignore_errors=True)\n if stale_path.exists():\n try:\n stale_path.unlink()\n except Exception:\n pass\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n logger.reason(f\"Cloning {remote_url} to {repo_path}\", extra={\"src\": \"init_repo\"})\n repo = self._clone_with_auth(remote_url, repo_path, pat)\n self._ensure_gitflow_branches(repo, dashboard_id)\n return repo\n # endregion init_repo\n\n # region delete_repo [C:4] [TYPE Function] [SEMANTICS git,delete,lock]\n # @PURPOSE Remove local repository and DB binding for a dashboard with concurrent access protection.\n # @PRE dashboard_id is a valid integer.\n # @POST Local path is deleted when present and GitRepository row is removed.\n # @SIDE_EFFECT Deletes filesystem directory and DB record; protected by per-dashboard lock.\n def delete_repo(self, dashboard_id: int) -> None:\n with self._locked(dashboard_id), belief_scope(\"GitService.delete_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n removed_files = False\n if os.path.exists(repo_path):\n if os.path.isdir(repo_path):\n shutil.rmtree(repo_path)\n else:\n os.remove(repo_path)\n removed_files = True\n session = SessionLocal()\n try:\n db_repo = (\n session.query(GitRepository)\n .filter(GitRepository.dashboard_id == int(dashboard_id))\n .first()\n )\n if db_repo:\n session.delete(db_repo)\n session.commit()\n return\n if removed_files:\n return\n raise HTTPException(\n status_code=404,\n detail=f\"Repository for dashboard {dashboard_id} not found\",\n )\n except HTTPException:\n session.rollback()\n raise\n except Exception as e:\n session.rollback()\n logger.error(f\"[delete_repo][Coherence:Failed] Failed to delete repository for dashboard {dashboard_id}: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to delete repository: {e!s}\")\n finally:\n session.close()\n # endregion delete_repo\n\n # region get_repo [C:4] [TYPE Function] [SEMANTICS git,open,lock]\n # @PURPOSE Get Repo object for a dashboard with concurrent access protection.\n # @PRE Repository must exist on disk for the given dashboard_id.\n # @POST Returns a GitPython Repo instance for the dashboard.\n # @RETURN Repo\n def get_repo(self, dashboard_id: int) -> Repo:\n with self._locked(dashboard_id), belief_scope(\"GitService.get_repo\"):\n repo_path = self._get_repo_path(dashboard_id)\n if not os.path.exists(repo_path):\n logger.error(f\"[EXT:method:get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist\")\n raise HTTPException(status_code=404, detail=f\"Repository for dashboard {dashboard_id} not found\")\n try:\n return Repo(repo_path)\n except Exception as e:\n logger.error(f\"[EXT:method:get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}\")\n raise HTTPException(status_code=500, detail=\"Failed to open local Git repository\")\n # endregion get_repo\n\n # region configure_identity [C:4] [TYPE Function] [SEMANTICS git,identity,lock]\n # @PURPOSE Configure repository-local Git committer identity with concurrent access protection.\n # @PRE dashboard_id repository exists; git_username/git_email may be empty.\n # @POST Repository config has user.name and user.email when both identity values are provided.\n # @SIDE_EFFECT Writes to repository git config; protected by per-dashboard lock.\n def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:\n with self._locked(dashboard_id), belief_scope(\"GitService.configure_identity\"):\n normalized_username = str(git_username or \"\").strip()\n normalized_email = str(git_email or \"\").strip()\n if not normalized_username or not normalized_email:\n return\n repo = self.get_repo(dashboard_id)\n try:\n with repo.config_writer(config_level=\"repository\") as config_writer:\n config_writer.set_value(\"user\", \"name\", normalized_username)\n config_writer.set_value(\"user\", \"email\", normalized_email)\n logger.reason(f\"Applied repository-local git identity for dashboard {dashboard_id}\", extra={\"src\": \"configure_identity\"})\n except Exception as e:\n logger.error(f\"[configure_identity][Coherence:Failed] Failed to configure git identity: {e}\")\n raise HTTPException(status_code=500, detail=f\"Failed to configure git identity: {e!s}\")\n # endregion configure_identity\n\n # region close [C:2] [TYPE Function] [SEMANTICS http,cleanup,shutdown]\n # @BRIEF Gracefully close the shared HTTP client (connection pool).\n # @PRE _http_client is initialized.\n # @POST HTTP connections closed.\n async def close(self):\n with self._close_lock:\n if self._closed:\n return\n self._closed = True\n await self._http_client.aclose()\n # endregion close\n# #endregion GitServiceBase\n# #endregion GitServiceBase\n" }, { "contract_id": "GitServiceBranchMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_branch.py", "start_line": 1, - "end_line": 208, + "end_line": 256, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -46309,14 +47189,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceBranchMixin [C:4] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace, lock]\n# @LAYER Infrastructure\n# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks).\n# @RELATION CALLED_BY -> [GitService]\n\nfrom datetime import datetime\nimport os\n\nfrom fastapi import HTTPException\nfrom git import Repo\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceBranchMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing branch and commit operations for GitService.\nclass GitServiceBranchMixin:\n # region _ensure_gitflow_branches [C:4] [TYPE Function] [SEMANTICS git,gitflow,branch,lock]\n # @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.\n # @PRE repo is a valid GitPython Repo instance.\n # @POST main, dev, preprod are available in local repository and pushed to origin when available.\n # Active branch unchanged (no spurious checkout).\n def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:\n with belief_scope(\"GitService._ensure_gitflow_branches\"):\n required_branches = [\"main\", \"dev\", \"preprod\"]\n local_heads = {head.name: head for head in getattr(repo, \"heads\", [])}\n base_commit = None\n try:\n base_commit = repo.head.commit\n except Exception:\n base_commit = None\n if \"main\" in local_heads:\n base_commit = local_heads[\"main\"].commit\n if base_commit is None:\n logger.reason(\n f\"Skipping branch bootstrap for dashboard {dashboard_id}: repository has no commits\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n logger.reason(f\"Created local branch main for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in (\"dev\", \"preprod\"):\n if branch_name in local_heads:\n continue\n local_heads[branch_name] = repo.create_head(branch_name, local_heads[\"main\"].commit)\n logger.reason(\n f\"Created local branch {branch_name} for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n logger.reason(\n f\"Remote origin is not configured for dashboard {dashboard_id}; skipping remote branch creation\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n remote_branch_names = set()\n try:\n origin.fetch()\n for ref in origin.refs:\n remote_head = getattr(ref, \"remote_head\", None)\n if remote_head:\n remote_branch_names.add(str(remote_head))\n except Exception as e:\n logger.reason(f\"Failed to fetch origin refs: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in required_branches:\n if branch_name in remote_branch_names:\n continue\n try:\n origin.push(refspec=f\"{branch_name}:{branch_name}\")\n logger.reason(\n f\"Pushed branch {branch_name} to origin for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n except Exception as e:\n logger.error(f\"[_ensure_gitflow_branches][Coherence:Failed] Failed to push branch {branch_name} to origin: {e}\")\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {e!s}\",\n )\n # Fix 6: Only checkout if not already on dev\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = None\n if current_branch != \"dev\":\n try:\n repo.git.checkout(\"dev\")\n logger.reason(f\"Checked out default branch dev for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n except Exception as e:\n logger.reason(f\"Could not checkout dev branch for dashboard {dashboard_id}: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n # endregion _ensure_gitflow_branches\n\n # region list_branches [C:4] [TYPE Function] [SEMANTICS git,branch,list,lock]\n # @PURPOSE: List all branches (excluding tags) for a dashboard's repository, concurrent-safe.\n # @PRE Repository for dashboard_id exists.\n # @POST Returns a list of branch metadata dictionaries (no tag refs).\n # @RETURN List[dict]\n def list_branches(self, dashboard_id: int) -> list[dict]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.list_branches\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Listing branches for {dashboard_id}. Refs: {repo.refs}\", extra={\"src\": \"list_branches\"})\n branches = []\n for ref in repo.refs:\n try:\n # Fix 8: Skip tag refs\n ref_name = str(ref.name)\n if ref_name.startswith('refs/tags/'):\n continue\n name = ref_name.replace('refs/heads/', '').replace('refs/remotes/origin/', '')\n if any(b['name'] == name for b in branches):\n continue\n branches.append({\n \"name\": name,\n \"commit_hash\": ref.commit.hexsha if hasattr(ref, 'commit') else \"0000000\",\n \"is_remote\": ref.is_remote() if hasattr(ref, 'is_remote') else False,\n \"last_updated\": datetime.fromtimestamp(ref.commit.committed_date) if hasattr(ref, 'commit') else datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Skipping ref {ref}: {e}\", extra={\"src\": \"list_branches\"})\n try:\n active_name = repo.active_branch.name\n if not any(b['name'] == active_name for b in branches):\n branches.append({\n \"name\": active_name, \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n except Exception as e:\n logger.reason(f\"Could not determine active branch: {e}\", extra={\"src\": \"list_branches\"})\n if not branches:\n branches.append({\n \"name\": \"dev\", \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.utcnow()\n })\n return branches\n # endregion list_branches\n\n # region create_branch [C:4] [TYPE Function] [SEMANTICS git,branch,create,lock]\n # @PURPOSE: Create a new branch from an existing one (concurrent-safe).\n # @PARAM name (str) - New branch name.\n # @PARAM from_branch (str) - Source branch.\n # @PRE Repository exists; name is valid; from_branch exists or repo is empty.\n # @POST A new branch is created in the repository.\n def create_branch(self, dashboard_id: int, name: str, from_branch: str = \"main\"):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.create_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Creating branch {name} from {from_branch}\", extra={\"src\": \"create_branch\"})\n if not repo.heads and not repo.remotes:\n logger.reason(\"Repository is empty. Creating initial commit to enable branching.\", extra={\"src\": \"create_branch\"})\n readme_path = os.path.join(repo.working_dir, \"README.md\")\n if not os.path.exists(readme_path):\n with open(readme_path, \"w\") as f:\n f.write(f\"# Dashboard {dashboard_id}\\nGit repository for Superset dashboard integration.\")\n repo.index.add([\"README.md\"])\n repo.index.commit(\"Initial commit\")\n try:\n repo.commit(from_branch)\n except Exception:\n logger.reason(f\"Source branch {from_branch} not found, using HEAD\", extra={\"src\": \"create_branch\"})\n from_branch = repo.head\n try:\n new_branch = repo.create_head(name, from_branch)\n return new_branch\n except Exception as e:\n logger.error(f\"[create_branch][Coherence:Failed] {e}\")\n raise\n # endregion create_branch\n\n # region checkout_branch [C:4] [TYPE Function] [SEMANTICS git,branch,checkout,lock]\n # @PURPOSE: Switch to a specific branch (concurrent-safe).\n # @PRE Repository exists and the specified branch name exists.\n # @POST The repository working directory is updated to the specified branch.\n def checkout_branch(self, dashboard_id: int, name: str):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.checkout_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Checking out branch {name}\", extra={\"src\": \"checkout_branch\"})\n repo.git.checkout(name)\n # endregion checkout_branch\n\n # region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock]\n # @PURPOSE: Stage and commit changes (concurrent-safe).\n # @PARAM message (str) - Commit message.\n # @PARAM files (List[str]) - Optional list of specific files to stage.\n # @PRE Repository exists and has changes (dirty) or files are specified.\n # @POST Changes are staged and a new commit is created.\n def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.commit_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.is_dirty(untracked_files=True) and not files:\n logger.reason(f\"No changes to commit for dashboard {dashboard_id}\", extra={\"src\": \"commit_changes\"})\n return\n if files:\n logger.reason(f\"Staging files: {files}\", extra={\"src\": \"commit_changes\"})\n repo.index.add(files)\n else:\n logger.reason(\"Staging all changes\", extra={\"src\": \"commit_changes\"})\n repo.git.add(A=True)\n repo.index.commit(message)\n logger.info(f\"[commit_changes][Coherence:OK] Committed changes with message: {message}\")\n # endregion commit_changes\n# #endregion GitServiceBranchMixin\n# #endregion GitServiceBranchMixin\n" + "body": "# #region GitServiceBranchMixin [C:4] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace, lock]\n# @LAYER Infrastructure\n# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks).\n# @RELATION CALLED_BY -> [GitService]\n\nfrom datetime import UTC, datetime\nimport os\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import GitCommandError\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceBranchMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing branch and commit operations for GitService.\nclass GitServiceBranchMixin:\n # region _ensure_gitflow_branches [C:4] [TYPE Function] [SEMANTICS git,gitflow,branch,lock]\n # @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.\n # @PRE repo is a valid GitPython Repo instance.\n # @POST main, dev, preprod are available in local repository and pushed to origin when available.\n # Active branch unchanged (no spurious checkout).\n def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:\n with belief_scope(\"GitService._ensure_gitflow_branches\"):\n required_branches = [\"main\", \"dev\", \"preprod\"]\n local_heads = {head.name: head for head in getattr(repo, \"heads\", [])}\n base_commit = None\n try:\n base_commit = repo.head.commit\n except Exception:\n base_commit = None\n if \"main\" in local_heads:\n base_commit = local_heads[\"main\"].commit\n if base_commit is None:\n logger.reason(\n f\"Skipping branch bootstrap for dashboard {dashboard_id}: repository has no commits\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n if \"main\" not in local_heads:\n local_heads[\"main\"] = repo.create_head(\"main\", base_commit)\n logger.reason(f\"Created local branch main for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in (\"dev\", \"preprod\"):\n if branch_name in local_heads:\n continue\n local_heads[branch_name] = repo.create_head(branch_name, local_heads[\"main\"].commit)\n logger.reason(\n f\"Created local branch {branch_name} for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n logger.reason(\n f\"Remote origin is not configured for dashboard {dashboard_id}; skipping remote branch creation\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n return\n remote_branch_names = set()\n try:\n origin.fetch()\n for ref in origin.refs:\n remote_head = getattr(ref, \"remote_head\", None)\n if remote_head:\n remote_branch_names.add(str(remote_head))\n except Exception as e:\n logger.reason(f\"Failed to fetch origin refs: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n for branch_name in required_branches:\n if branch_name in remote_branch_names:\n continue\n try:\n origin.push(refspec=f\"{branch_name}:{branch_name}\")\n logger.reason(\n f\"Pushed branch {branch_name} to origin for dashboard {dashboard_id}\",\n extra={\"src\": \"_ensure_gitflow_branches\"},\n )\n except Exception as e:\n logger.error(f\"[_ensure_gitflow_branches][Coherence:Failed] Failed to push branch {branch_name} to origin: {e}\")\n raise HTTPException(\n status_code=500,\n detail=f\"Failed to create default branch '{branch_name}' on remote: {e!s}\",\n )\n # Fix 6: Only checkout if not already on dev\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = None\n if current_branch != \"dev\":\n try:\n repo.git.checkout(\"dev\")\n logger.reason(f\"Checked out default branch dev for dashboard {dashboard_id}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n except Exception as e:\n logger.reason(f\"Could not checkout dev branch for dashboard {dashboard_id}: {e}\", extra={\"src\": \"_ensure_gitflow_branches\"})\n # endregion _ensure_gitflow_branches\n\n # region list_branches [C:4] [TYPE Function] [SEMANTICS git,branch,list,lock]\n # @PURPOSE: List all branches (excluding tags) for a dashboard's repository, concurrent-safe.\n # @PRE Repository for dashboard_id exists.\n # @POST Returns a list of branch metadata dictionaries (no tag refs).\n # @RETURN List[dict]\n def list_branches(self, dashboard_id: int) -> list[dict]:\n with self._locked(dashboard_id), belief_scope(\"GitService.list_branches\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Listing branches for {dashboard_id}. Refs: {repo.refs}\", extra={\"src\": \"list_branches\"})\n branches = []\n for ref in repo.refs:\n try:\n # Fix 8: Skip tag refs\n ref_name = str(ref.name)\n if ref_name.startswith('refs/tags/'):\n continue\n name = ref_name.replace('refs/heads/', '').replace('refs/remotes/origin/', '')\n if any(b['name'] == name for b in branches):\n continue\n branches.append({\n \"name\": name,\n \"commit_hash\": ref.commit.hexsha if hasattr(ref, 'commit') else \"0000000\",\n \"is_remote\": ref.is_remote() if hasattr(ref, 'is_remote') else False,\n \"last_updated\": datetime.fromtimestamp(ref.commit.committed_date) if hasattr(ref, 'commit') else datetime.now(UTC)\n })\n except Exception as e:\n logger.reason(f\"Skipping ref {ref}: {e}\", extra={\"src\": \"list_branches\"})\n try:\n active_name = repo.active_branch.name\n if not any(b['name'] == active_name for b in branches):\n branches.append({\n \"name\": active_name, \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.now(UTC)\n })\n except Exception as e:\n logger.reason(f\"Could not determine active branch: {e}\", extra={\"src\": \"list_branches\"})\n if not branches:\n branches.append({\n \"name\": \"dev\", \"commit_hash\": \"0000000\",\n \"is_remote\": False, \"last_updated\": datetime.now(UTC)\n })\n return branches\n # endregion list_branches\n\n # region create_branch [C:4] [TYPE Function] [SEMANTICS git,branch,create,lock]\n # @PURPOSE: Create a new branch from an existing one (concurrent-safe).\n # @PARAM name (str) - New branch name.\n # @PARAM from_branch (str) - Source branch.\n # @PRE Repository exists; name is valid; from_branch exists or repo is empty.\n # @POST A new branch is created in the repository.\n def create_branch(self, dashboard_id: int, name: str, from_branch: str = \"main\"):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.create_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Creating branch {name} from {from_branch}\", extra={\"src\": \"create_branch\"})\n if not repo.heads and not repo.remotes:\n logger.reason(\"Repository is empty. Creating initial commit to enable branching.\", extra={\"src\": \"create_branch\"})\n readme_path = os.path.join(repo.working_dir, \"README.md\")\n if not os.path.exists(readme_path):\n with open(readme_path, \"w\") as f:\n f.write(f\"# Dashboard {dashboard_id}\\nGit repository for Superset dashboard integration.\")\n repo.index.add([\"README.md\"])\n repo.index.commit(\"Initial commit\")\n try:\n repo.commit(from_branch)\n except Exception:\n logger.reason(f\"Source branch {from_branch} not found, using HEAD\", extra={\"src\": \"create_branch\"})\n from_branch = repo.head\n try:\n new_branch = repo.create_head(name, from_branch)\n return new_branch\n except Exception as e:\n logger.error(f\"[create_branch][Coherence:Failed] {e}\")\n raise\n # endregion create_branch\n\n # region checkout_branch [C:4] [TYPE Function] [SEMANTICS git,branch,checkout,lock]\n # @PURPOSE: Switch to a specific branch (concurrent-safe).\n # @PRE Repository exists and the specified branch name exists.\n # @POST The repository working directory is updated to the specified branch.\n # @SIDE_EFFECT May raise HTTPException(409) if local changes conflict with checkout.\n # @SIDE_EFFECT May raise HTTPException(500) if Git operation fails for other reasons.\n def checkout_branch(self, dashboard_id: int, name: str):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.checkout_branch\"):\n repo = self.get_repo(dashboard_id)\n logger.reason(f\"Checking out branch {name}\", extra={\"src\": \"checkout_branch\"})\n try:\n repo.git.checkout(name)\n except GitCommandError as e:\n stderr = str(e.stderr or \"\")\n details = str(e)\n lowered = stderr.lower()\n if \"local changes\" in lowered or \"would be overwritten\" in lowered:\n # Parse the list of files from stderr\n files = []\n for line in stderr.split(\"\\n\"):\n stripped = line.strip()\n if stripped.startswith(\"\\t\") and (\n stripped.endswith(\".yaml\") or \"/\" in stripped\n ):\n files.append(stripped.strip())\n raise HTTPException(\n status_code=409,\n detail={\n \"error_code\": \"GIT_CHECKOUT_LOCAL_CHANGES\",\n \"message\": (\n f\"Невозможно переключиться на ветку '{name}' — \"\n f\"локальные изменения будут перезаписаны. \"\n f\"Зафиксируйте или отложите изменения.\"\n ),\n \"message_en\": (\n f\"Cannot checkout branch '{name}' — \"\n f\"local changes would be overwritten. \"\n f\"Commit or stash your changes first.\"\n ),\n \"files\": files,\n \"next_steps\": [\n \"Зафиксируйте изменения (Commit) в текущей ветке перед переключением\",\n \"Отложите изменения через Stash (вручную: git stash)\",\n \"Отмените локальные изменения, если они не нужны\",\n ],\n \"next_steps_en\": [\n \"Commit your changes in the current branch before switching\",\n \"Stash your changes manually: git stash\",\n \"Discard local changes if not needed\",\n ],\n },\n )\n logger.error(f\"[checkout_branch][Coherence:Failed] {e}\")\n raise HTTPException(\n status_code=500,\n detail=f\"Git checkout failed: {details}\",\n )\n # endregion checkout_branch\n\n # region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock]\n # @PURPOSE: Stage and commit changes (concurrent-safe).\n # @PARAM message (str) - Commit message.\n # @PARAM files (List[str]) - Optional list of specific files to stage.\n # @PRE Repository exists and has changes (dirty) or files are specified.\n # @POST Changes are staged and a new commit is created.\n def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.commit_changes\"):\n repo = self.get_repo(dashboard_id)\n if not repo.is_dirty(untracked_files=True) and not files:\n logger.reason(f\"No changes to commit for dashboard {dashboard_id}\", extra={\"src\": \"commit_changes\"})\n return\n if files:\n logger.reason(f\"Staging files: {files}\", extra={\"src\": \"commit_changes\"})\n repo.index.add(files)\n else:\n logger.reason(\"Staging all changes\", extra={\"src\": \"commit_changes\"})\n repo.git.add(A=True)\n repo.index.commit(message)\n logger.info(f\"[commit_changes][Coherence:OK] Committed changes with message: {message}\")\n # endregion commit_changes\n# #endregion GitServiceBranchMixin\n# #endregion GitServiceBranchMixin\n" }, { "contract_id": "GitServiceGiteaMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_gitea.py", "start_line": 1, - "end_line": 271, + "end_line": 270, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -46335,14 +47215,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceGiteaMixin [C:4] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection, http_pool]\n# @LAYER Infrastructure\n# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling.\n# @RELATION CALLED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\nimport httpx\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitProvider\n\n\n# #region GitServiceGiteaMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing Gitea API operations for GitService.\nclass GitServiceGiteaMixin:\n # region test_connection [TYPE Function]\n # @PURPOSE: Test connection to Git provider using PAT.\n # @PARAM provider (GitProvider), url (str), pat (str)\n # @PRE provider is valid; url is a valid HTTP(S) URL; pat is provided.\n # @POST Returns True if connection to the provider's API succeeds.\n # @RETURN bool\n async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:\n with belief_scope(\"GitService.test_connection\"):\n if \".local\" in url or \"localhost\" in url:\n logger.reason(\"Local/Offline mode detected for URL\", extra={\"src\": \"test_connection\"})\n return True\n if not url.startswith(('http://', 'https://')):\n logger.error(f\"[test_connection][Coherence:Failed] Invalid URL protocol: {url}\")\n return False\n if not pat or not pat.strip():\n logger.error(\"[test_connection][Coherence:Failed] Git PAT is missing or empty\")\n return False\n pat = pat.strip()\n try:\n if provider == GitProvider.GITHUB:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = \"https://api.github.com/user\" if \"github.com\" in url else f\"{url.rstrip('/')}/api/v3/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITLAB:\n headers = {\"PRIVATE-TOKEN\": pat}\n api_url = f\"{url.rstrip('/')}/api/v4/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITEA:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = f\"{url.rstrip('/')}/api/v1/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n else:\n return False\n if resp.status_code != 200:\n logger.error(f\"[test_connection][Coherence:Failed] Git connection test failed for {provider} at {api_url}. Status: {resp.status_code}\")\n return resp.status_code == 200\n except Exception as e:\n logger.error(f\"[test_connection][Coherence:Failed] Error testing git connection: {e}\")\n return False\n # endregion test_connection\n\n # region _gitea_headers [TYPE Function]\n # @PURPOSE: Build Gitea API authorization headers.\n # @PRE pat is provided.\n # @POST Returns headers with token auth.\n # @RETURN Dict[str, str]\n def _gitea_headers(self, pat: str) -> dict[str, str]:\n token = (pat or \"\").strip()\n if not token:\n raise HTTPException(status_code=400, detail=\"Git PAT is required for Gitea operations\")\n return {\n \"Authorization\": f\"token {token}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n }\n # endregion _gitea_headers\n\n # region _gitea_request [TYPE Function]\n # @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.\n # @PRE method and endpoint are valid.\n # @POST Returns decoded JSON payload.\n # @RETURN Any\n async def _gitea_request(\n self, method: str, server_url: str, pat: str, endpoint: str,\n payload: dict[str, Any] | None = None,\n ) -> Any:\n base_url = self._normalize_git_server_url(server_url)\n url = f\"{base_url}/api/v1{endpoint}\"\n headers = self._gitea_headers(pat)\n try:\n response = await self._http_client.request(method=method, url=url, headers=headers, json=payload)\n except Exception as e:\n logger.error(f\"[gitea_request][Coherence:Failed] Network error: {e}\")\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or parsed.get(\"error\") or detail\n except Exception:\n logger.debug(\"Could not parse error response JSON from Gitea API\")\n logger.error(f\"[gitea_request][Coherence:Failed] method={method} endpoint={endpoint} status={response.status_code} detail={detail}\")\n raise HTTPException(status_code=response.status_code, detail=f\"Gitea API error: {detail}\")\n if response.status_code == 204:\n return None\n return response.json()\n # endregion _gitea_request\n\n # region get_gitea_current_user [TYPE Function]\n # @PURPOSE: Resolve current Gitea user for PAT.\n # @PRE server_url and pat are valid.\n # @POST Returns current username.\n # @RETURN str\n async def get_gitea_current_user(self, server_url: str, pat: str) -> str:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user\")\n username = payload.get(\"login\") or payload.get(\"username\")\n if not username:\n raise HTTPException(status_code=500, detail=\"Failed to resolve Gitea username\")\n return str(username)\n # endregion get_gitea_current_user\n\n # region list_gitea_repositories [TYPE Function]\n # @PURPOSE: List repositories visible to authenticated Gitea user.\n # @PRE server_url and pat are valid.\n # @POST Returns repository list from Gitea.\n # @RETURN List[dict]\n async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user/repos?limit=100&page=1\")\n if not isinstance(payload, list):\n return []\n return payload\n # endregion list_gitea_repositories\n\n # region create_gitea_repository [TYPE Function]\n # @PURPOSE: Create repository in Gitea for authenticated user. Idempotent — returns existing repo if name already taken.\n # @PRE name is non-empty and PAT has repo creation permission.\n # @POST Returns created or existing repository payload.\n # @RETURN dict\n async def create_gitea_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n created = await self._gitea_request(\"POST\", server_url, pat, \"/user/repos\", payload=payload)\n except HTTPException as exc:\n if exc.status_code == 409:\n logger.reason(\n f\"Repository '{name}' already exists on Gitea, fetching existing repo\",\n extra={\"src\": \"create_gitea_repository\"},\n )\n username = await self.get_gitea_current_user(server_url, pat)\n existing = await self._gitea_request(\"GET\", server_url, pat, f\"/repos/{username}/{name}\")\n if isinstance(existing, dict):\n return existing\n raise\n if not isinstance(created, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating repository\")\n return created\n # endregion create_gitea_repository\n\n # region delete_gitea_repository [TYPE Function]\n # @PURPOSE: Delete repository in Gitea.\n # @PRE owner and repo_name are non-empty.\n # @POST Repository deleted on Gitea server.\n async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:\n if not owner or not repo_name:\n raise HTTPException(status_code=400, detail=\"owner and repo_name are required\")\n await self._gitea_request(\"DELETE\", server_url, pat, f\"/repos/{owner}/{repo_name}\")\n # endregion delete_gitea_repository\n\n # region _gitea_branch_exists [TYPE Function]\n # @PURPOSE: Check whether a branch exists in Gitea repository.\n # @PRE owner/repo/branch are non-empty.\n # @POST Returns True when branch exists, False when 404.\n # @RETURN bool\n async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:\n if not owner or not repo or not branch:\n return False\n endpoint = f\"/repos/{owner}/{repo}/branches/{quote(branch, safe='')}\"\n try:\n await self._gitea_request(\"GET\", server_url, pat, endpoint)\n return True\n except HTTPException as exc:\n if exc.status_code == 404:\n return False\n raise\n # endregion _gitea_branch_exists\n\n # region _build_gitea_pr_404_detail [TYPE Function]\n # @PURPOSE: Build actionable error detail for Gitea PR 404 responses.\n # @PRE owner/repo/from_branch/to_branch are provided.\n # @POST Returns specific branch-missing message when detected.\n # @RETURN Optional[str]\n async def _build_gitea_pr_404_detail(\n self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,\n ) -> str | None:\n source_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=from_branch,\n )\n target_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=to_branch,\n )\n if not source_exists:\n return f\"Gitea branch not found: source branch '{from_branch}' in {owner}/{repo}\"\n if not target_exists:\n return f\"Gitea branch not found: target branch '{to_branch}' in {owner}/{repo}\"\n return None\n # endregion _build_gitea_pr_404_detail\n\n # region create_gitea_pull_request [TYPE Function]\n # @PURPOSE: Create pull request in Gitea.\n # @PRE Config and remote URL are valid.\n # @POST Returns normalized PR metadata.\n # @RETURN Dict[str, Any]\n async def create_gitea_pull_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n req_payload = {\"title\": title, \"head\": from_branch, \"base\": to_branch, \"body\": description or \"\"}\n endpoint = f\"/repos/{identity['owner']}/{identity['repo']}/pulls\"\n active_server_url = server_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as exc:\n fallback_url = self._derive_server_url_from_remote(remote_url)\n normalized_primary = self._normalize_git_server_url(server_url)\n should_retry_with_fallback = (\n exc.status_code == 404 and fallback_url and fallback_url != normalized_primary\n )\n if should_retry_with_fallback:\n logger.explore(\n f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\",\n extra={\"src\": \"create_gitea_pull_request\"},\n )\n active_server_url = fallback_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as retry_exc:\n if retry_exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n else:\n if exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n if not isinstance(data, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating pull request\")\n return {\n \"id\": data.get(\"number\") or data.get(\"id\"),\n \"url\": data.get(\"html_url\") or data.get(\"url\"),\n \"status\": data.get(\"state\") or \"open\",\n }\n # endregion create_gitea_pull_request\n# #endregion GitServiceGiteaMixin\n# #endregion GitServiceGiteaMixin\n" + "body": "# #region GitServiceGiteaMixin [C:4] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection, http_pool]\n# @LAYER Infrastructure\n# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling.\n# @RELATION CALLED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\n\nfrom src.core.logger import belief_scope, logger\nfrom src.models.git import GitProvider\n\n\n# #region GitServiceGiteaMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing Gitea API operations for GitService.\nclass GitServiceGiteaMixin:\n # region test_connection [TYPE Function]\n # @PURPOSE: Test connection to Git provider using PAT.\n # @PARAM provider (GitProvider), url (str), pat (str)\n # @PRE provider is valid; url is a valid HTTP(S) URL; pat is provided.\n # @POST Returns True if connection to the provider's API succeeds.\n # @RETURN bool\n async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:\n with belief_scope(\"GitService.test_connection\"):\n if \".local\" in url or \"localhost\" in url:\n logger.reason(\"Local/Offline mode detected for URL\", extra={\"src\": \"test_connection\"})\n return True\n if not url.startswith(('http://', 'https://')):\n logger.error(f\"[test_connection][Coherence:Failed] Invalid URL protocol: {url}\")\n return False\n if not pat or not pat.strip():\n logger.error(\"[test_connection][Coherence:Failed] Git PAT is missing or empty\")\n return False\n pat = pat.strip()\n try:\n if provider == GitProvider.GITHUB:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = \"https://api.github.com/user\" if \"github.com\" in url else f\"{url.rstrip('/')}/api/v3/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITLAB:\n headers = {\"PRIVATE-TOKEN\": pat}\n api_url = f\"{url.rstrip('/')}/api/v4/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n elif provider == GitProvider.GITEA:\n headers = {\"Authorization\": f\"token {pat}\"}\n api_url = f\"{url.rstrip('/')}/api/v1/user\"\n resp = await self._http_client.get(api_url, headers=headers)\n else:\n return False\n if resp.status_code != 200:\n logger.error(f\"[test_connection][Coherence:Failed] Git connection test failed for {provider} at {api_url}. Status: {resp.status_code}\")\n return resp.status_code == 200\n except Exception as e:\n logger.error(f\"[test_connection][Coherence:Failed] Error testing git connection: {e}\")\n return False\n # endregion test_connection\n\n # region _gitea_headers [TYPE Function]\n # @PURPOSE: Build Gitea API authorization headers.\n # @PRE pat is provided.\n # @POST Returns headers with token auth.\n # @RETURN Dict[str, str]\n def _gitea_headers(self, pat: str) -> dict[str, str]:\n token = (pat or \"\").strip()\n if not token:\n raise HTTPException(status_code=400, detail=\"Git PAT is required for Gitea operations\")\n return {\n \"Authorization\": f\"token {token}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json\",\n }\n # endregion _gitea_headers\n\n # region _gitea_request [TYPE Function]\n # @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.\n # @PRE method and endpoint are valid.\n # @POST Returns decoded JSON payload.\n # @RETURN Any\n async def _gitea_request(\n self, method: str, server_url: str, pat: str, endpoint: str,\n payload: dict[str, Any] | None = None,\n ) -> Any:\n base_url = self._normalize_git_server_url(server_url)\n url = f\"{base_url}/api/v1{endpoint}\"\n headers = self._gitea_headers(pat)\n try:\n response = await self._http_client.request(method=method, url=url, headers=headers, json=payload)\n except Exception as e:\n logger.error(f\"[gitea_request][Coherence:Failed] Network error: {e}\")\n raise HTTPException(status_code=503, detail=f\"Gitea API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or parsed.get(\"error\") or detail\n except Exception:\n logger.debug(\"Could not parse error response JSON from Gitea API\")\n logger.error(f\"[gitea_request][Coherence:Failed] method={method} endpoint={endpoint} status={response.status_code} detail={detail}\")\n raise HTTPException(status_code=response.status_code, detail=f\"Gitea API error: {detail}\")\n if response.status_code == 204:\n return None\n return response.json()\n # endregion _gitea_request\n\n # region get_gitea_current_user [TYPE Function]\n # @PURPOSE: Resolve current Gitea user for PAT.\n # @PRE server_url and pat are valid.\n # @POST Returns current username.\n # @RETURN str\n async def get_gitea_current_user(self, server_url: str, pat: str) -> str:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user\")\n username = payload.get(\"login\") or payload.get(\"username\")\n if not username:\n raise HTTPException(status_code=500, detail=\"Failed to resolve Gitea username\")\n return str(username)\n # endregion get_gitea_current_user\n\n # region list_gitea_repositories [TYPE Function]\n # @PURPOSE: List repositories visible to authenticated Gitea user.\n # @PRE server_url and pat are valid.\n # @POST Returns repository list from Gitea.\n # @RETURN List[dict]\n async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:\n payload = await self._gitea_request(\"GET\", server_url, pat, \"/user/repos?limit=100&page=1\")\n if not isinstance(payload, list):\n return []\n return payload\n # endregion list_gitea_repositories\n\n # region create_gitea_repository [TYPE Function]\n # @PURPOSE: Create repository in Gitea for authenticated user. Idempotent — returns existing repo if name already taken.\n # @PRE name is non-empty and PAT has repo creation permission.\n # @POST Returns created or existing repository payload.\n # @RETURN dict\n async def create_gitea_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n created = await self._gitea_request(\"POST\", server_url, pat, \"/user/repos\", payload=payload)\n except HTTPException as exc:\n if exc.status_code == 409:\n logger.reason(\n f\"Repository '{name}' already exists on Gitea, fetching existing repo\",\n extra={\"src\": \"create_gitea_repository\"},\n )\n username = await self.get_gitea_current_user(server_url, pat)\n existing = await self._gitea_request(\"GET\", server_url, pat, f\"/repos/{username}/{name}\")\n if isinstance(existing, dict):\n return existing\n raise\n if not isinstance(created, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating repository\")\n return created\n # endregion create_gitea_repository\n\n # region delete_gitea_repository [TYPE Function]\n # @PURPOSE: Delete repository in Gitea.\n # @PRE owner and repo_name are non-empty.\n # @POST Repository deleted on Gitea server.\n async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:\n if not owner or not repo_name:\n raise HTTPException(status_code=400, detail=\"owner and repo_name are required\")\n await self._gitea_request(\"DELETE\", server_url, pat, f\"/repos/{owner}/{repo_name}\")\n # endregion delete_gitea_repository\n\n # region _gitea_branch_exists [TYPE Function]\n # @PURPOSE: Check whether a branch exists in Gitea repository.\n # @PRE owner/repo/branch are non-empty.\n # @POST Returns True when branch exists, False when 404.\n # @RETURN bool\n async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:\n if not owner or not repo or not branch:\n return False\n endpoint = f\"/repos/{owner}/{repo}/branches/{quote(branch, safe='')}\"\n try:\n await self._gitea_request(\"GET\", server_url, pat, endpoint)\n return True\n except HTTPException as exc:\n if exc.status_code == 404:\n return False\n raise\n # endregion _gitea_branch_exists\n\n # region _build_gitea_pr_404_detail [TYPE Function]\n # @PURPOSE: Build actionable error detail for Gitea PR 404 responses.\n # @PRE owner/repo/from_branch/to_branch are provided.\n # @POST Returns specific branch-missing message when detected.\n # @RETURN Optional[str]\n async def _build_gitea_pr_404_detail(\n self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,\n ) -> str | None:\n source_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=from_branch,\n )\n target_exists = await self._gitea_branch_exists(\n server_url=server_url, pat=pat, owner=owner, repo=repo, branch=to_branch,\n )\n if not source_exists:\n return f\"Gitea branch not found: source branch '{from_branch}' in {owner}/{repo}\"\n if not target_exists:\n return f\"Gitea branch not found: target branch '{to_branch}' in {owner}/{repo}\"\n return None\n # endregion _build_gitea_pr_404_detail\n\n # region create_gitea_pull_request [TYPE Function]\n # @PURPOSE: Create pull request in Gitea.\n # @PRE Config and remote URL are valid.\n # @POST Returns normalized PR metadata.\n # @RETURN Dict[str, Any]\n async def create_gitea_pull_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n req_payload = {\"title\": title, \"head\": from_branch, \"base\": to_branch, \"body\": description or \"\"}\n endpoint = f\"/repos/{identity['owner']}/{identity['repo']}/pulls\"\n active_server_url = server_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as exc:\n fallback_url = self._derive_server_url_from_remote(remote_url)\n normalized_primary = self._normalize_git_server_url(server_url)\n should_retry_with_fallback = (\n exc.status_code == 404 and fallback_url and fallback_url != normalized_primary\n )\n if should_retry_with_fallback:\n logger.explore(\n f\"Primary Gitea URL not found, retrying with remote host: {fallback_url}\",\n extra={\"src\": \"create_gitea_pull_request\"},\n )\n active_server_url = fallback_url\n try:\n data = await self._gitea_request(\"POST\", active_server_url, pat, endpoint, payload=req_payload)\n except HTTPException as retry_exc:\n if retry_exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n else:\n if exc.status_code == 404:\n branch_detail = await self._build_gitea_pr_404_detail(\n server_url=active_server_url, pat=pat,\n owner=identity[\"owner\"], repo=identity[\"repo\"],\n from_branch=from_branch, to_branch=to_branch,\n )\n if branch_detail:\n raise HTTPException(status_code=400, detail=branch_detail)\n raise\n if not isinstance(data, dict):\n raise HTTPException(status_code=500, detail=\"Unexpected Gitea response while creating pull request\")\n return {\n \"id\": data.get(\"number\") or data.get(\"id\"),\n \"url\": data.get(\"html_url\") or data.get(\"url\"),\n \"status\": data.get(\"state\") or \"open\",\n }\n # endregion create_gitea_pull_request\n# #endregion GitServiceGiteaMixin\n# #endregion GitServiceGiteaMixin\n" }, { "contract_id": "GitServiceMergeMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_merge.py", "start_line": 1, - "end_line": 301, + "end_line": 300, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -46361,14 +47241,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]\n# @LAYER Infrastructure\n# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).\n# @RELATION CALLED_BY -> [GitService]\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom git.objects.blob import Blob\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceMergeMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing merge operations for GitService.\nclass GitServiceMergeMixin:\n # region _read_blob_text [TYPE 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 logger.debug(\"[_read_blob_text] Could not decode blob text\")\n return \"\"\n # endregion _read_blob_text\n\n # region _get_unmerged_file_paths [TYPE 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 # endregion _get_unmerged_file_paths\n\n # region _build_unfinished_merge_payload [TYPE 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 # endregion _build_unfinished_merge_payload\n\n # region get_merge_status [C:4] [TYPE Function] [SEMANTICS git,merge,status,lock]\n # @PURPOSE: Get current merge status for a dashboard repository (concurrent-safe).\n def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_status\"):\n repo = self.get_repo(dashboard_id)\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n if not os.path.exists(merge_head_path):\n current_branch = \"unknown\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n return {\n \"has_unfinished_merge\": False,\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": None,\n \"merge_message_preview\": None,\n \"conflicts_count\": 0,\n }\n payload = self._build_unfinished_merge_payload(repo)\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": payload[\"repository_path\"],\n \"git_dir\": payload[\"git_dir\"],\n \"current_branch\": payload[\"current_branch\"],\n \"merge_head\": payload[\"merge_head\"],\n \"merge_message_preview\": payload[\"merge_message_preview\"],\n \"conflicts_count\": int(payload.get(\"conflicts_count\") or 0),\n }\n # endregion get_merge_status\n\n # region get_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,list,lock]\n # @PURPOSE: List all files with conflicts and their contents (concurrent-safe).\n def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n conflicts = []\n unmerged = repo.index.unmerged_blobs()\n for file_path, stages in unmerged.items():\n mine_blob = None\n theirs_blob = None\n for stage, blob in stages:\n if stage == 2:\n mine_blob = blob\n elif stage == 3:\n theirs_blob = blob\n conflicts.append({\n \"file_path\": file_path,\n \"mine\": self._read_blob_text(mine_blob) if mine_blob else \"\",\n \"theirs\": self._read_blob_text(theirs_blob) if theirs_blob else \"\",\n })\n return sorted(conflicts, key=lambda item: item[\"file_path\"])\n # endregion get_merge_conflicts\n\n # region resolve_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,resolve,lock]\n # @PURPOSE: Resolve conflicts using specified strategy (concurrent-safe).\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions: list[dict[str, Any]]) -> list[str]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.resolve_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n resolved_files: list[str] = []\n repo_root = os.path.abspath(str(repo.working_tree_dir or \"\"))\n if not repo_root:\n raise HTTPException(status_code=500, detail=\"Repository working tree directory is unavailable\")\n for item in resolutions or []:\n file_path = str(item.get(\"file_path\") or \"\").strip()\n strategy = str(item.get(\"resolution\") or \"\").strip().lower()\n content = item.get(\"content\")\n if not file_path:\n raise HTTPException(status_code=400, detail=\"resolution.file_path is required\")\n if strategy not in {\"mine\", \"theirs\", \"manual\"}:\n raise HTTPException(status_code=400, detail=f\"Unsupported resolution strategy: {strategy}\")\n if strategy == \"mine\":\n repo.git.checkout(\"--ours\", \"--\", file_path)\n elif strategy == \"theirs\":\n repo.git.checkout(\"--theirs\", \"--\", file_path)\n else:\n abs_target = os.path.abspath(os.path.join(repo_root, file_path))\n if abs_target != repo_root and not abs_target.startswith(repo_root + os.sep):\n raise HTTPException(status_code=400, detail=f\"Invalid conflict file path: {file_path}\")\n os.makedirs(os.path.dirname(abs_target), exist_ok=True)\n with open(abs_target, \"w\", encoding=\"utf-8\") as file_obj:\n file_obj.write(str(content or \"\"))\n repo.git.add(file_path)\n resolved_files.append(file_path)\n return resolved_files\n # endregion resolve_merge_conflicts\n\n # region abort_merge [C:4] [TYPE Function] [SEMANTICS git,merge,abort,lock]\n # @PURPOSE: Abort ongoing merge (concurrent-safe).\n def abort_merge(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.abort_merge\"):\n repo = self.get_repo(dashboard_id)\n try:\n repo.git.merge(\"--abort\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"there is no merge to abort\" in lowered or \"no merge to abort\" in lowered:\n return {\"status\": \"no_merge_in_progress\"}\n raise HTTPException(status_code=409, detail=f\"Cannot abort merge: {details}\")\n return {\"status\": \"aborted\"}\n # endregion abort_merge\n\n # region continue_merge [C:4] [TYPE Function] [SEMANTICS git,merge,continue,lock]\n # @PURPOSE: Finalize merge after conflict resolution (concurrent-safe).\n def continue_merge(self, dashboard_id: int, message: str | None = None) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.continue_merge\"):\n repo = self.get_repo(dashboard_id)\n unmerged_files = self._get_unmerged_file_paths(repo)\n if unmerged_files:\n raise HTTPException(\n status_code=409,\n detail={\n \"error_code\": \"GIT_MERGE_CONFLICTS_REMAIN\",\n \"message\": \"\\u041d\\u0435\\u0432\\u043e\\u0437\\u043c\\u043e\\u0436\\u043d\\u043e \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u044c merge: \\u043e\\u0441\\u0442\\u0430\\u043b\\u0438\\u0441\\u044c \\u043d\\u0435\\u0440\\u0430\\u0437\\u0440\\u0435\\u0448\\u0451\\u043d\\u043d\\u044b\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b.\",\n \"unresolved_files\": unmerged_files,\n },\n )\n try:\n normalized_message = str(message or \"\").strip()\n if normalized_message:\n repo.git.commit(\"-m\", normalized_message)\n else:\n repo.git.commit(\"--no-edit\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"nothing to commit\" in lowered:\n return {\"status\": \"already_clean\"}\n raise HTTPException(status_code=409, detail=f\"Cannot continue merge: {details}\")\n commit_hash = \"\"\n try:\n commit_hash = repo.head.commit.hexsha\n except Exception:\n commit_hash = \"\"\n return {\"status\": \"committed\", \"commit_hash\": commit_hash}\n # endregion continue_merge\n\n # region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]\n # @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.\n # @PRE Repository exists and both branches are valid.\n # @POST Target branch contains merged changes from source branch. Active branch restored to original.\n # Merge survives locally even if push fails (partial success).\n # @SIDE_EFFECT Changes local branch state during merge; restores original branch in finally block.\n # @RETURN Dict[str, Any]\n def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.promote_direct_merge\"):\n if not from_branch or not to_branch:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch are required\")\n repo = self.get_repo(dashboard_id)\n source = from_branch.strip()\n target = to_branch.strip()\n if source == target:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch must be different\")\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n\n # Remember original branch for restoration in finally\n original_branch = None\n try:\n original_branch = repo.active_branch.name\n except Exception:\n original_branch = None\n\n try:\n origin.fetch()\n if source not in [head.name for head in repo.heads]:\n if f\"origin/{source}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", source, f\"origin/{source}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Source branch '{source}' not found\")\n if target in [head.name for head in repo.heads]:\n repo.git.checkout(target)\n elif f\"origin/{target}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", target, f\"origin/{target}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Target branch '{target}' not found\")\n try:\n origin.pull(target)\n except Exception:\n logger.debug(\"Could not pull target branch %s before direct promote\", target)\n repo.git.merge(source, \"--no-ff\", \"-m\", f\"chore(flow): promote {source} -> {target}\")\n origin.push(refspec=f\"{target}:{target}\")\n except HTTPException:\n raise\n except Exception as e:\n message = str(e)\n if \"CONFLICT\" in message.upper():\n raise HTTPException(status_code=409, detail=f\"Merge conflict during direct promote: {message}\")\n raise HTTPException(status_code=500, detail=f\"Direct promote failed: {message}\")\n finally:\n # Restore original branch even if merge/push partially failed\n if original_branch:\n try:\n repo.git.checkout(original_branch)\n except Exception:\n logger.debug(\"Could not restore original branch %s after direct promote\", original_branch)\n return {\"mode\": \"direct\", \"from_branch\": source, \"to_branch\": target, \"status\": \"merged\"}\n # endregion promote_direct_merge\n# #endregion GitServiceMergeMixin\n# #endregion GitServiceMergeMixin\n" + "body": "# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]\n# @LAYER Infrastructure\n# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).\n# @RELATION CALLED_BY -> [GitService]\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom fastapi import HTTPException\nfrom git import Repo\nfrom git.exc import GitCommandError\nfrom git.objects.blob import Blob\n\nfrom src.core.logger import belief_scope, logger\n\n\n# #region GitServiceMergeMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing merge operations for GitService.\nclass GitServiceMergeMixin:\n # region _read_blob_text [TYPE 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 logger.debug(\"[_read_blob_text] Could not decode blob text\")\n return \"\"\n # endregion _read_blob_text\n\n # region _get_unmerged_file_paths [TYPE 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 # endregion _get_unmerged_file_paths\n\n # region _build_unfinished_merge_payload [TYPE 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 # endregion _build_unfinished_merge_payload\n\n # region get_merge_status [C:4] [TYPE Function] [SEMANTICS git,merge,status,lock]\n # @PURPOSE: Get current merge status for a dashboard repository (concurrent-safe).\n def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_status\"):\n repo = self.get_repo(dashboard_id)\n merge_head_path = os.path.join(repo.git_dir, \"MERGE_HEAD\")\n if not os.path.exists(merge_head_path):\n current_branch = \"unknown\"\n try:\n current_branch = repo.active_branch.name\n except Exception:\n current_branch = \"detached_or_unknown\"\n return {\n \"has_unfinished_merge\": False,\n \"repository_path\": repo.working_tree_dir,\n \"git_dir\": repo.git_dir,\n \"current_branch\": current_branch,\n \"merge_head\": None,\n \"merge_message_preview\": None,\n \"conflicts_count\": 0,\n }\n payload = self._build_unfinished_merge_payload(repo)\n return {\n \"has_unfinished_merge\": True,\n \"repository_path\": payload[\"repository_path\"],\n \"git_dir\": payload[\"git_dir\"],\n \"current_branch\": payload[\"current_branch\"],\n \"merge_head\": payload[\"merge_head\"],\n \"merge_message_preview\": payload[\"merge_message_preview\"],\n \"conflicts_count\": int(payload.get(\"conflicts_count\") or 0),\n }\n # endregion get_merge_status\n\n # region get_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,list,lock]\n # @PURPOSE: List all files with conflicts and their contents (concurrent-safe).\n def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.get_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n conflicts = []\n unmerged = repo.index.unmerged_blobs()\n for file_path, stages in unmerged.items():\n mine_blob = None\n theirs_blob = None\n for stage, blob in stages:\n if stage == 2:\n mine_blob = blob\n elif stage == 3:\n theirs_blob = blob\n conflicts.append({\n \"file_path\": file_path,\n \"mine\": self._read_blob_text(mine_blob) if mine_blob else \"\",\n \"theirs\": self._read_blob_text(theirs_blob) if theirs_blob else \"\",\n })\n return sorted(conflicts, key=lambda item: item[\"file_path\"])\n # endregion get_merge_conflicts\n\n # region resolve_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,resolve,lock]\n # @PURPOSE: Resolve conflicts using specified strategy (concurrent-safe).\n def resolve_merge_conflicts(self, dashboard_id: int, resolutions: list[dict[str, Any]]) -> list[str]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.resolve_merge_conflicts\"):\n repo = self.get_repo(dashboard_id)\n resolved_files: list[str] = []\n repo_root = os.path.abspath(str(repo.working_tree_dir or \"\"))\n if not repo_root:\n raise HTTPException(status_code=500, detail=\"Repository working tree directory is unavailable\")\n for item in resolutions or []:\n file_path = str(item.get(\"file_path\") or \"\").strip()\n strategy = str(item.get(\"resolution\") or \"\").strip().lower()\n content = item.get(\"content\")\n if not file_path:\n raise HTTPException(status_code=400, detail=\"resolution.file_path is required\")\n if strategy not in {\"mine\", \"theirs\", \"manual\"}:\n raise HTTPException(status_code=400, detail=f\"Unsupported resolution strategy: {strategy}\")\n if strategy == \"mine\":\n repo.git.checkout(\"--ours\", \"--\", file_path)\n elif strategy == \"theirs\":\n repo.git.checkout(\"--theirs\", \"--\", file_path)\n else:\n abs_target = os.path.abspath(os.path.join(repo_root, file_path))\n if abs_target != repo_root and not abs_target.startswith(repo_root + os.sep):\n raise HTTPException(status_code=400, detail=f\"Invalid conflict file path: {file_path}\")\n os.makedirs(os.path.dirname(abs_target), exist_ok=True)\n with open(abs_target, \"w\", encoding=\"utf-8\") as file_obj:\n file_obj.write(str(content or \"\"))\n repo.git.add(file_path)\n resolved_files.append(file_path)\n return resolved_files\n # endregion resolve_merge_conflicts\n\n # region abort_merge [C:4] [TYPE Function] [SEMANTICS git,merge,abort,lock]\n # @PURPOSE: Abort ongoing merge (concurrent-safe).\n def abort_merge(self, dashboard_id: int) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.abort_merge\"):\n repo = self.get_repo(dashboard_id)\n try:\n repo.git.merge(\"--abort\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"there is no merge to abort\" in lowered or \"no merge to abort\" in lowered:\n return {\"status\": \"no_merge_in_progress\"}\n raise HTTPException(status_code=409, detail=f\"Cannot abort merge: {details}\")\n return {\"status\": \"aborted\"}\n # endregion abort_merge\n\n # region continue_merge [C:4] [TYPE Function] [SEMANTICS git,merge,continue,lock]\n # @PURPOSE: Finalize merge after conflict resolution (concurrent-safe).\n def continue_merge(self, dashboard_id: int, message: str | None = None) -> dict[str, Any]:\n with self._locked(dashboard_id):\n with belief_scope(\"GitService.continue_merge\"):\n repo = self.get_repo(dashboard_id)\n unmerged_files = self._get_unmerged_file_paths(repo)\n if unmerged_files:\n raise HTTPException(\n status_code=409,\n detail={\n \"error_code\": \"GIT_MERGE_CONFLICTS_REMAIN\",\n \"message\": \"\\u041d\\u0435\\u0432\\u043e\\u0437\\u043c\\u043e\\u0436\\u043d\\u043e \\u0437\\u0430\\u0432\\u0435\\u0440\\u0448\\u0438\\u0442\\u044c merge: \\u043e\\u0441\\u0442\\u0430\\u043b\\u0438\\u0441\\u044c \\u043d\\u0435\\u0440\\u0430\\u0437\\u0440\\u0435\\u0448\\u0451\\u043d\\u043d\\u044b\\u0435 \\u043a\\u043e\\u043d\\u0444\\u043b\\u0438\\u043a\\u0442\\u044b.\",\n \"unresolved_files\": unmerged_files,\n },\n )\n try:\n normalized_message = str(message or \"\").strip()\n if normalized_message:\n repo.git.commit(\"-m\", normalized_message)\n else:\n repo.git.commit(\"--no-edit\")\n except GitCommandError as e:\n details = str(e)\n lowered = details.lower()\n if \"nothing to commit\" in lowered:\n return {\"status\": \"already_clean\"}\n raise HTTPException(status_code=409, detail=f\"Cannot continue merge: {details}\")\n commit_hash = \"\"\n try:\n commit_hash = repo.head.commit.hexsha\n except Exception:\n commit_hash = \"\"\n return {\"status\": \"committed\", \"commit_hash\": commit_hash}\n # endregion continue_merge\n\n # region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]\n # @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.\n # @PRE Repository exists and both branches are valid.\n # @POST Target branch contains merged changes from source branch. Active branch restored to original.\n # Merge survives locally even if push fails (partial success).\n # @SIDE_EFFECT Changes local branch state during merge; restores original branch in finally block.\n # @RETURN Dict[str, Any]\n def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:\n with self._locked(dashboard_id), belief_scope(\"GitService.promote_direct_merge\"):\n if not from_branch or not to_branch:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch are required\")\n repo = self.get_repo(dashboard_id)\n source = from_branch.strip()\n target = to_branch.strip()\n if source == target:\n raise HTTPException(status_code=400, detail=\"from_branch and to_branch must be different\")\n try:\n origin = repo.remote(name=\"origin\")\n except ValueError:\n raise HTTPException(status_code=400, detail=\"Remote 'origin' not configured\")\n\n # Remember original branch for restoration in finally\n original_branch = None\n try:\n original_branch = repo.active_branch.name\n except Exception:\n original_branch = None\n\n try:\n origin.fetch()\n if source not in [head.name for head in repo.heads]:\n if f\"origin/{source}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", source, f\"origin/{source}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Source branch '{source}' not found\")\n if target in [head.name for head in repo.heads]:\n repo.git.checkout(target)\n elif f\"origin/{target}\" in [ref.name for ref in repo.refs]:\n repo.git.checkout(\"-b\", target, f\"origin/{target}\")\n else:\n raise HTTPException(status_code=404, detail=f\"Target branch '{target}' not found\")\n try:\n origin.pull(target)\n except Exception:\n logger.debug(\"Could not pull target branch %s before direct promote\", target)\n repo.git.merge(source, \"--no-ff\", \"-m\", f\"chore(flow): promote {source} -> {target}\")\n origin.push(refspec=f\"{target}:{target}\")\n except HTTPException:\n raise\n except Exception as e:\n message = str(e)\n if \"CONFLICT\" in message.upper():\n raise HTTPException(status_code=409, detail=f\"Merge conflict during direct promote: {message}\")\n raise HTTPException(status_code=500, detail=f\"Direct promote failed: {message}\")\n finally:\n # Restore original branch even if merge/push partially failed\n if original_branch:\n try:\n repo.git.checkout(original_branch)\n except Exception:\n logger.debug(\"Could not restore original branch %s after direct promote\", original_branch)\n return {\"mode\": \"direct\", \"from_branch\": source, \"to_branch\": target, \"status\": \"merged\"}\n # endregion promote_direct_merge\n# #endregion GitServiceMergeMixin\n# #endregion GitServiceMergeMixin\n" }, { "contract_id": "GitServiceRemoteMixin", "contract_type": "Module", "file_path": "backend/src/services/git/_remote_providers.py", "start_line": 1, - "end_line": 141, + "end_line": 140, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -46387,14 +47267,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region GitServiceRemoteMixin [C:4] [TYPE Module] [SEMANTICS git, provider, github, remote, url, http_pool]\n# @LAYER Infrastructure\n# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling.\n# @RELATION CALLED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\nimport httpx\n\nfrom src.core.logger import logger\n\n\n# #region GitServiceGithubMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitHub API operations for GitService.\nclass GitServiceGithubMixin:\n # region create_github_repository [TYPE Function]\n # @PURPOSE: Create repository in GitHub or GitHub Enterprise.\n # @PRE PAT has repository create permission.\n # @POST Returns created repository payload.\n # @RETURN dict\n async def create_github_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n if \"github.com\" in base_url:\n api_url = \"https://api.github.com/user/repos\"\n else:\n api_url = f\"{base_url}/api/v3/user/repos\"\n headers = {\n \"Authorization\": f\"token {pat.strip()}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/vnd.github+json\",\n }\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitHub API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or detail\n except Exception:\n logger.debug(\"Could not parse error response JSON from GitHub API\")\n raise HTTPException(status_code=response.status_code, detail=f\"GitHub API error: {detail}\")\n return response.json()\n # endregion create_github_pull_request\n# #endregion GitServiceGithubMixin\n\n# #region GitServiceGitlabMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # region create_gitlab_repository [TYPE Function]\n # @PURPOSE: Create repository(project) in GitLab.\n # @PRE PAT has api scope.\n # @POST Returns created repository payload.\n # @RETURN dict\n async def create_gitlab_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n api_url = f\"{base_url}/api/v4/projects\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload: dict[str, Any] = {\n \"name\": name, \"visibility\": \"private\" if private else \"public\",\n \"initialize_with_readme\": bool(auto_init),\n }\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n if \"clone_url\" not in data:\n data[\"clone_url\"] = data.get(\"http_url_to_repo\")\n if \"html_url\" not in data:\n data[\"html_url\"] = data.get(\"web_url\")\n if \"ssh_url\" not in data:\n data[\"ssh_url\"] = data.get(\"ssh_url_to_repo\")\n if \"full_name\" not in data:\n data[\"full_name\"] = data.get(\"path_with_namespace\") or data.get(\"name\")\n return data\n # endregion create_gitlab_repository\n\n # region create_gitlab_merge_request [TYPE Function]\n # @PURPOSE: Create merge request in GitLab.\n # @PRE Config and remote URL are valid.\n # @POST Returns normalized MR metadata.\n # @RETURN Dict[str, Any]\n async def create_gitlab_merge_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None, remove_source_branch: bool = False,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n base_url = self._normalize_git_server_url(server_url)\n project_id = quote(identity[\"full_name\"], safe=\"\")\n api_url = f\"{base_url}/api/v4/projects/{project_id}/merge_requests\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload = {\n \"source_branch\": from_branch, \"target_branch\": to_branch, \"title\": title,\n \"description\": description or \"\", \"remove_source_branch\": bool(remove_source_branch),\n }\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"iid\") or data.get(\"id\"), \"url\": data.get(\"web_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"opened\"}\n # endregion create_gitlab_merge_request\n# #endregion GitServiceGitlabMixin\n# #endregion GitServiceRemoteMixin\n" + "body": "# #region GitServiceRemoteMixin [C:4] [TYPE Module] [SEMANTICS git, provider, github, remote, url, http_pool]\n# @LAYER Infrastructure\n# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling.\n# @RELATION CALLED_BY -> [GitService]\n\nfrom typing import Any\nfrom urllib.parse import quote\n\nfrom fastapi import HTTPException\n\nfrom src.core.logger import logger\n\n\n# #region GitServiceGithubMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitHub API operations for GitService.\nclass GitServiceGithubMixin:\n # region create_github_repository [TYPE Function]\n # @PURPOSE: Create repository in GitHub or GitHub Enterprise.\n # @PRE PAT has repository create permission.\n # @POST Returns created repository payload.\n # @RETURN dict\n async def create_github_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n if \"github.com\" in base_url:\n api_url = \"https://api.github.com/user/repos\"\n else:\n api_url = f\"{base_url}/api/v3/user/repos\"\n headers = {\n \"Authorization\": f\"token {pat.strip()}\",\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/vnd.github+json\",\n }\n payload: dict[str, Any] = {\"name\": name, \"private\": bool(private), \"auto_init\": bool(auto_init)}\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitHub API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n detail = parsed.get(\"message\") or detail\n except Exception:\n logger.debug(\"Could not parse error response JSON from GitHub API\")\n raise HTTPException(status_code=response.status_code, detail=f\"GitHub API error: {detail}\")\n return response.json()\n # endregion create_github_pull_request\n# #endregion GitServiceGithubMixin\n\n# #region GitServiceGitlabMixin [C:3] [TYPE Class]\n# @BRIEF Mixin providing GitLab API operations for GitService.\nclass GitServiceGitlabMixin:\n # region create_gitlab_repository [TYPE Function]\n # @PURPOSE: Create repository(project) in GitLab.\n # @PRE PAT has api scope.\n # @POST Returns created repository payload.\n # @RETURN dict\n async def create_gitlab_repository(\n self, server_url: str, pat: str, name: str, private: bool = True,\n description: str | None = None, auto_init: bool = True, default_branch: str | None = \"main\",\n ) -> dict[str, Any]:\n base_url = self._normalize_git_server_url(server_url)\n api_url = f\"{base_url}/api/v4/projects\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload: dict[str, Any] = {\n \"name\": name, \"visibility\": \"private\" if private else \"public\",\n \"initialize_with_readme\": bool(auto_init),\n }\n if description:\n payload[\"description\"] = description\n if default_branch:\n payload[\"default_branch\"] = default_branch\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n if \"clone_url\" not in data:\n data[\"clone_url\"] = data.get(\"http_url_to_repo\")\n if \"html_url\" not in data:\n data[\"html_url\"] = data.get(\"web_url\")\n if \"ssh_url\" not in data:\n data[\"ssh_url\"] = data.get(\"ssh_url_to_repo\")\n if \"full_name\" not in data:\n data[\"full_name\"] = data.get(\"path_with_namespace\") or data.get(\"name\")\n return data\n # endregion create_gitlab_repository\n\n # region create_gitlab_merge_request [TYPE Function]\n # @PURPOSE: Create merge request in GitLab.\n # @PRE Config and remote URL are valid.\n # @POST Returns normalized MR metadata.\n # @RETURN Dict[str, Any]\n async def create_gitlab_merge_request(\n self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,\n title: str, description: str | None = None, remove_source_branch: bool = False,\n ) -> dict[str, Any]:\n identity = self._parse_remote_repo_identity(remote_url)\n base_url = self._normalize_git_server_url(server_url)\n project_id = quote(identity[\"full_name\"], safe=\"\")\n api_url = f\"{base_url}/api/v4/projects/{project_id}/merge_requests\"\n headers = {\"PRIVATE-TOKEN\": pat.strip(), \"Content-Type\": \"application/json\", \"Accept\": \"application/json\"}\n payload = {\n \"source_branch\": from_branch, \"target_branch\": to_branch, \"title\": title,\n \"description\": description or \"\", \"remove_source_branch\": bool(remove_source_branch),\n }\n try:\n response = await self._http_client.post(api_url, headers=headers, json=payload)\n except Exception as e:\n raise HTTPException(status_code=503, detail=f\"GitLab API is unavailable: {e!s}\")\n if response.status_code >= 400:\n detail = response.text\n try:\n parsed = response.json()\n if isinstance(parsed, dict):\n detail = parsed.get(\"message\") or detail\n except Exception:\n pass\n raise HTTPException(status_code=response.status_code, detail=f\"GitLab API error: {detail}\")\n data = response.json()\n return {\"id\": data.get(\"iid\") or data.get(\"id\"), \"url\": data.get(\"web_url\") or data.get(\"url\"), \"status\": data.get(\"state\") or \"opened\"}\n # endregion create_gitlab_merge_request\n# #endregion GitServiceGitlabMixin\n# #endregion GitServiceRemoteMixin\n" }, { "contract_id": "GitServiceGithubMixin", "contract_type": "Class", "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 15, - "end_line": 56, + "start_line": 14, + "end_line": 55, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -46411,8 +47291,8 @@ "contract_id": "GitServiceGitlabMixin", "contract_type": "Class", "file_path": "backend/src/services/git/_remote_providers.py", - "start_line": 58, - "end_line": 140, + "start_line": 57, + "end_line": 139, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -46982,7 +47862,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceBannerRenderer [C:3] [TYPE Module] [SEMANTICS maintenance, banner, renderer, text]\n# @BRIEF Banner text rendering and rebuild helpers. Build aggregated markdown from\n# active maintenance events, format with template, apply HTML escaping.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n\nimport html\nimport re as _re\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\n\n\n# #region build_banner_text [C:3] [TYPE Function] [SEMANTICS maintenance, banner, text, template, rendering]\n# @BRIEF Build aggregated banner markdown text from events using settings template.\n# Single event: substitute {start_time}, {end_time}, {message} directly.\n# Multiple events: render each event as sub-block, combine as {message}.\n# Missing variables → empty string. Message is HTML-escaped.\n# @PRE events is a non-empty list of dicts with start_time, end_time, message keys.\n# template is a non-empty string.\n# @POST Returns str with substituted template or empty string on failure.\ndef build_banner_text(\n events: list[dict[str, Any]],\n template: str,\n display_timezone: str = \"UTC\",\n) -> str:\n with belief_scope(\n \"build_banner_text\",\n f\"events={len(events)}\",\n ):\n if not events:\n app_logger.reflect(\"No events, returning empty string\")\n return \"\"\n\n def _format_dt(dt_str: str | None) -> str:\n \"\"\"Format datetime string for display.\"\"\"\n if not dt_str:\n return \"уточняется\"\n try:\n dt = datetime.fromisoformat(dt_str.replace(\"Z\", \"+00:00\"))\n return dt.strftime(\"%Y-%m-%d %H:%M\")\n except (ValueError, TypeError):\n return dt_str or \"уточняется\"\n\n def _event_substitutions(event: dict[str, Any]) -> dict[str, str]:\n \"\"\"Build substitution dict for a single event.\"\"\"\n msg = event.get(\"message\") or \"\"\n escaped_msg = html.escape(msg) if msg else \"\"\n return {\n \"start_time\": _format_dt(event.get(\"start_time\")),\n \"end_time\": _format_dt(event.get(\"end_time\")),\n \"message\": escaped_msg,\n }\n\n if len(events) == 1:\n subs = _event_substitutions(events[0])\n result = template\n for key, value in subs.items():\n result = result.replace(f\"{{{key}}}\", value)\n # Remove any remaining unreplaced variables\n result = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", result)\n app_logger.reflect(\"Banner text built (single event)\")\n return result\n\n # Multiple events: build aggregated message, substitute as {message}\n event_blocks: list[str] = []\n for i, event in enumerate(events):\n subs = _event_substitutions(event)\n block = template\n for key, value in subs.items():\n block = block.replace(f\"{{{key}}}\", value)\n block = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", block)\n event_blocks.append(block)\n\n combined_message = \"\\n\\n---\\n\\n\".join(event_blocks)\n\n # Build aggregator template (use first event for start/end if uniform)\n result = template\n result = result.replace(\"{message}\", combined_message)\n if len(events) == 1:\n subs = _event_substitutions(events[0])\n for key in (\"start_time\", \"end_time\"):\n result = result.replace(f\"{{{key}}}\", subs.get(key, \"\"))\n else:\n result = result.replace(\"{start_time}\", \"\")\n result = result.replace(\"{end_time}\", \"\")\n result = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", result)\n\n app_logger.reflect(\"Banner text built (multi-event)\")\n return result\n# #endregion build_banner_text\n\n\n# #region _build_banner_text_for_dashboard [C:2] [TYPE Function]\n# @BRIEF Build aggregated banner text for a banner based on all active events linked to it.\ndef _build_banner_text_for_dashboard(\n banner_id: str,\n db_session: Session,\n template: str,\n timezone_str: str,\n) -> str:\n try:\n # Find all active dashboard states linked to this banner\n active_states = (\n db_session.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.banner_id == banner_id,\n MaintenanceDashboardState.status == MaintenanceDashboardStateStatus.ACTIVE,\n )\n .all()\n )\n\n if not active_states:\n return \"\"\n\n # Load events for each state\n events_data: list[dict[str, Any]] = []\n for state in active_states:\n ev = (\n db_session.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == state.event_id)\n .first()\n )\n if ev and ev.status in (\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.APPLYING,\n ):\n events_data.append(\n {\n \"start_time\": ev.start_time.isoformat() if ev.start_time else None,\n \"end_time\": ev.end_time.isoformat() if ev.end_time else None,\n \"message\": ev.message or \"\",\n }\n )\n\n if not events_data:\n return \"\"\n\n return build_banner_text(events_data, template, timezone_str)\n except Exception as e:\n app_logger.explore(\n \"Failed to build banner text\",\n extra={\"banner_id\": banner_id},\n error=str(e),\n )\n return \"\"\n# #endregion _build_banner_text_for_dashboard\n\n\n# #region rebuild_banner [C:3] [TYPE Function] [SEMANTICS maintenance, banner, rebuild, text, update]\n# @BRIEF Rebuild aggregated banner text for a banner and update the Superset chart.\n# Query all active MaintenanceDashboardState records linked to banner,\n# build aggregated text via build_banner_text, update markdown chart in Superset.\n# @PRE banner_id points to an active MaintenanceDashboardBanner with a valid chart_id.\n# @POST Banner chart in Superset updated with current aggregated text. DB banner_text updated.\n# @SIDE_EFFECT Modifies Superset chart. Writes DB.\n# @RELATION DEPENDS_ON -> [build_banner_text]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\ndef rebuild_banner(\n banner_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> bool:\n with belief_scope(\"rebuild_banner\", f\"banner_id={banner_id}\"):\n app_logger.reason(\n \"Rebuilding banner text\",\n extra={\"banner_id\": banner_id},\n )\n\n banner = db_session.query(MaintenanceDashboardBanner).filter(\n MaintenanceDashboardBanner.id == banner_id\n ).first()\n if not banner:\n app_logger.explore(\n \"Banner not found\",\n extra={\"banner_id\": banner_id},\n error=\"BannerNotFound\",\n )\n return False\n\n if banner.status != MaintenanceDashboardBannerStatus.ACTIVE:\n app_logger.reflect(\n \"Banner not active, skipping\",\n extra={\"banner_id\": banner_id, \"status\": banner.status.value},\n )\n return False\n\n # Load settings for template\n settings = db_session.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n template = settings.banner_template if settings else \"\"\n timezone_str = settings.display_timezone if settings else \"UTC\"\n\n # Build aggregated text\n banner_text = _build_banner_text_for_dashboard(\n banner_id, db_session, template, timezone_str\n )\n\n if not banner_text and banner.chart_id:\n # No active events — should be handled by end_maintenance, but clean up just in case\n app_logger.reflect(\n \"No active events for banner, marking as REMOVED\",\n extra={\"banner_id\": banner_id},\n )\n banner.status = MaintenanceDashboardBannerStatus.REMOVED\n db_session.flush()\n return True\n\n # Update banner on dashboard (native MARKDOWN in layout)\n if banner.chart_id:\n try:\n superset_client.update_banner_on_dashboard(\n banner.dashboard_id, banner.chart_id, banner_text\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to update banner on dashboard during rebuild\",\n extra={\n \"chart_id\": banner.chart_id,\n \"banner_id\": banner_id,\n \"dashboard_id\": banner.dashboard_id,\n },\n error=str(e),\n )\n return False\n\n # Update DB\n banner.banner_text = banner_text\n db_session.flush()\n\n app_logger.reflect(\n \"Banner rebuilt successfully\",\n extra={\"banner_id\": banner_id},\n )\n return True\n# #endregion rebuild_banner\n\n# #endregion MaintenanceBannerRenderer\n" + "body": "# #region MaintenanceBannerRenderer [C:3] [TYPE Module] [SEMANTICS maintenance, banner, renderer, text]\n# @BRIEF Banner text rendering and rebuild helpers. Build aggregated markdown from\n# active maintenance events, format with template, apply HTML escaping.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n\nfrom datetime import datetime\nimport html\nimport re as _re\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\n\n\n# #region build_banner_text [C:3] [TYPE Function] [SEMANTICS maintenance, banner, text, template, rendering]\n# @BRIEF Build aggregated banner markdown text from events using settings template.\n# Single event: substitute {start_time}, {end_time}, {message} directly.\n# Multiple events: render each event as sub-block, combine as {message}.\n# Missing variables → empty string. Message is HTML-escaped.\n# @PRE events is a non-empty list of dicts with start_time, end_time, message keys.\n# template is a non-empty string.\n# @POST Returns str with substituted template or empty string on failure.\ndef build_banner_text(\n events: list[dict[str, Any]],\n template: str,\n display_timezone: str = \"UTC\",\n) -> str:\n with belief_scope(\n \"build_banner_text\",\n f\"events={len(events)}\",\n ):\n if not events:\n app_logger.reflect(\"No events, returning empty string\")\n return \"\"\n\n def _format_dt(dt_str: str | None) -> str:\n \"\"\"Format datetime string for display.\"\"\"\n if not dt_str:\n return \"уточняется\"\n try:\n dt = datetime.fromisoformat(dt_str.replace(\"Z\", \"+00:00\"))\n return dt.strftime(\"%Y-%m-%d %H:%M\")\n except (ValueError, TypeError):\n return dt_str or \"уточняется\"\n\n def _event_substitutions(event: dict[str, Any]) -> dict[str, str]:\n \"\"\"Build substitution dict for a single event.\"\"\"\n msg = event.get(\"message\") or \"\"\n escaped_msg = html.escape(msg) if msg else \"\"\n return {\n \"start_time\": _format_dt(event.get(\"start_time\")),\n \"end_time\": _format_dt(event.get(\"end_time\")),\n \"message\": escaped_msg,\n }\n\n if len(events) == 1:\n subs = _event_substitutions(events[0])\n result = template\n for key, value in subs.items():\n result = result.replace(f\"{{{key}}}\", value)\n # Remove any remaining unreplaced variables\n result = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", result)\n app_logger.reflect(\"Banner text built (single event)\")\n return result\n\n # Multiple events: build aggregated message, substitute as {message}\n event_blocks: list[str] = []\n for i, event in enumerate(events):\n subs = _event_substitutions(event)\n block = template\n for key, value in subs.items():\n block = block.replace(f\"{{{key}}}\", value)\n block = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", block)\n event_blocks.append(block)\n\n combined_message = \"\\n\\n---\\n\\n\".join(event_blocks)\n\n # Build aggregator template (use first event for start/end if uniform)\n result = template\n result = result.replace(\"{message}\", combined_message)\n if len(events) == 1:\n subs = _event_substitutions(events[0])\n for key in (\"start_time\", \"end_time\"):\n result = result.replace(f\"{{{key}}}\", subs.get(key, \"\"))\n else:\n result = result.replace(\"{start_time}\", \"\")\n result = result.replace(\"{end_time}\", \"\")\n result = _re.sub(r\"\\{[a-zA-Z_]+\\}\", \"\", result)\n\n app_logger.reflect(\"Banner text built (multi-event)\")\n return result\n# #endregion build_banner_text\n\n\n# #region _build_banner_text_for_dashboard [C:2] [TYPE Function]\n# @BRIEF Build aggregated banner text for a banner based on all active events linked to it.\ndef _build_banner_text_for_dashboard(\n banner_id: str,\n db_session: Session,\n template: str,\n timezone_str: str,\n) -> str:\n try:\n # Find all active dashboard states linked to this banner\n active_states = (\n db_session.query(MaintenanceDashboardState)\n .filter(\n MaintenanceDashboardState.banner_id == banner_id,\n MaintenanceDashboardState.status == MaintenanceDashboardStateStatus.ACTIVE,\n )\n .all()\n )\n\n if not active_states:\n return \"\"\n\n # Load events for each state\n events_data: list[dict[str, Any]] = []\n for state in active_states:\n ev = (\n db_session.query(MaintenanceEvent)\n .filter(MaintenanceEvent.id == state.event_id)\n .first()\n )\n if ev and ev.status in (\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.APPLYING,\n ):\n events_data.append(\n {\n \"start_time\": ev.start_time.isoformat() if ev.start_time else None,\n \"end_time\": ev.end_time.isoformat() if ev.end_time else None,\n \"message\": ev.message or \"\",\n }\n )\n\n if not events_data:\n return \"\"\n\n return build_banner_text(events_data, template, timezone_str)\n except Exception as e:\n app_logger.explore(\n \"Failed to build banner text\",\n extra={\"banner_id\": banner_id},\n error=str(e),\n )\n return \"\"\n# #endregion _build_banner_text_for_dashboard\n\n\n# #region rebuild_banner [C:3] [TYPE Function] [SEMANTICS maintenance, banner, rebuild, text, update]\n# @BRIEF Rebuild aggregated banner text for a banner and update the Superset chart.\n# Query all active MaintenanceDashboardState records linked to banner,\n# build aggregated text via build_banner_text, update markdown chart in Superset.\n# @PRE banner_id points to an active MaintenanceDashboardBanner with a valid chart_id.\n# @POST Banner chart in Superset updated with current aggregated text. DB banner_text updated.\n# @SIDE_EFFECT Modifies Superset chart. Writes DB.\n# @RELATION DEPENDS_ON -> [build_banner_text]\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\ndef rebuild_banner(\n banner_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> bool:\n with belief_scope(\"rebuild_banner\", f\"banner_id={banner_id}\"):\n app_logger.reason(\n \"Rebuilding banner text\",\n extra={\"banner_id\": banner_id},\n )\n\n banner = db_session.query(MaintenanceDashboardBanner).filter(\n MaintenanceDashboardBanner.id == banner_id\n ).first()\n if not banner:\n app_logger.explore(\n \"Banner not found\",\n extra={\"banner_id\": banner_id},\n error=\"BannerNotFound\",\n )\n return False\n\n if banner.status != MaintenanceDashboardBannerStatus.ACTIVE:\n app_logger.reflect(\n \"Banner not active, skipping\",\n extra={\"banner_id\": banner_id, \"status\": banner.status.value},\n )\n return False\n\n # Load settings for template\n settings = db_session.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n template = settings.banner_template if settings else \"\"\n timezone_str = settings.display_timezone if settings else \"UTC\"\n\n # Build aggregated text\n banner_text = _build_banner_text_for_dashboard(\n banner_id, db_session, template, timezone_str\n )\n\n if not banner_text and banner.chart_id:\n # No active events — should be handled by end_maintenance, but clean up just in case\n app_logger.reflect(\n \"No active events for banner, marking as REMOVED\",\n extra={\"banner_id\": banner_id},\n )\n banner.status = MaintenanceDashboardBannerStatus.REMOVED\n db_session.flush()\n return True\n\n # Update banner on dashboard (native MARKDOWN in layout)\n if banner.chart_id:\n try:\n superset_client.update_banner_on_dashboard(\n banner.dashboard_id, banner.chart_id, banner_text\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to update banner on dashboard during rebuild\",\n extra={\n \"chart_id\": banner.chart_id,\n \"banner_id\": banner_id,\n \"dashboard_id\": banner.dashboard_id,\n },\n error=str(e),\n )\n return False\n\n # Update DB\n banner.banner_text = banner_text\n db_session.flush()\n\n app_logger.reflect(\n \"Banner rebuilt successfully\",\n extra={\"banner_id\": banner_id},\n )\n return True\n# #endregion rebuild_banner\n\n# #endregion MaintenanceBannerRenderer\n" }, { "contract_id": "build_banner_text", @@ -47043,7 +47923,7 @@ "contract_type": "Module", "file_path": "backend/src/services/maintenance/_chart_manager.py", "start_line": 1, - "end_line": 359, + "end_line": 356, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47054,14 +47934,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceChartManager [C:3] [TYPE Module] [SEMANTICS maintenance, chart, banner, lifecycle]\n# @BRIEF Chart operations for maintenance banners: create/get banner charts, process\n# per-dashboard state transitions for start and end orchestrators.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom ._banner_renderer import _build_banner_text_for_dashboard, rebuild_banner\nfrom ._dashboard_scanner import _resolve_dashboard_title\n\n\n# #region ensure_banner_chart [C:3] [TYPE Function] [SEMANTICS maintenance, banner, chart, creation]\n# @BRIEF Get or create a MaintenanceDashboardBanner for a dashboard in the target environment.\n# If an active banner exists in DB, return it. Otherwise, create a markdown chart in Superset,\n# update dashboard layout, and store the banner row.\n# @PRE dashboard_id exists in Superset. superset_client has write access.\n# @POST Returns MaintenanceDashboardBanner with chart_id set. Chart is placed at top of dashboard.\n# @SIDE_EFFECT Creates chart in Superset, modifies dashboard layout, writes DB row.\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\ndef ensure_banner_chart(\n dashboard_id: int,\n environment_id: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> MaintenanceDashboardBanner:\n with belief_scope(\n \"ensure_banner_chart\",\n f\"dashboard_id={dashboard_id}\",\n ):\n app_logger.reason(\n \"Ensuring banner chart exists\",\n extra={\"dashboard_id\": dashboard_id},\n )\n\n # Check for existing active banner\n existing = (\n db_session.query(MaintenanceDashboardBanner)\n .filter(\n MaintenanceDashboardBanner.dashboard_id == dashboard_id,\n MaintenanceDashboardBanner.environment_id == environment_id,\n MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,\n )\n .first()\n )\n if existing:\n app_logger.reason(\n \"Found existing active banner, verifying chart in Superset\",\n extra={\"banner_id\": existing.id, \"chart_id\": existing.chart_id},\n )\n # Verify the chart still exists in Superset\n try:\n superset_client.get_chart(existing.chart_id)\n app_logger.reflect(\n \"Existing banner chart is alive, reusing\",\n extra={\"banner_id\": existing.id, \"chart_id\": existing.chart_id},\n )\n return existing\n except Exception:\n app_logger.explore(\n \"Existing banner chart is stale (not found in Superset), marking as REMOVED\",\n extra={\n \"banner_id\": existing.id,\n \"chart_id\": existing.chart_id,\n },\n )\n existing.status = MaintenanceDashboardBannerStatus.REMOVED\n db_session.flush()\n # Fall through to create a new chart\n\n # Create markdown chart in Superset\n try:\n chart_id = superset_client.create_markdown_chart(\n dashboard_id, \"*Maintenance banner placeholder*\"\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to create markdown chart\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n\n # Insert chart at top of dashboard layout\n try:\n superset_client.update_dashboard_layout(dashboard_id, chart_id)\n except Exception as e:\n app_logger.explore(\n \"Failed to update dashboard layout, deleting chart\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n error=str(e),\n )\n # Clean up the chart we just created\n try:\n superset_client.delete_chart(chart_id)\n except Exception:\n pass\n raise\n\n # Persist banner row\n banner = MaintenanceDashboardBanner(\n environment_id=environment_id,\n dashboard_id=dashboard_id,\n chart_id=chart_id,\n banner_text=\"\",\n status=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n db_session.add(banner)\n db_session.flush()\n\n app_logger.reflect(\n \"Banner chart created and persisted\",\n extra={\n \"banner_id\": banner.id,\n \"chart_id\": chart_id,\n \"dashboard_id\": dashboard_id,\n },\n )\n return banner\n# #endregion ensure_banner_chart\n\n\n# #region _process_dashboards_for_start [C:3] [TYPE Function]\n# @BRIEF Process each dashboard for a start maintenance event: ensure banner chart,\n# create dashboard state, build banner text, update chart markdown.\n# @SIDE_EFFECT Creates/updates Superset charts, writes DB rows.\n# @RELATION CALLS -> [ensure_banner_chart]\n# @RELATION CALLS -> [_build_banner_text_for_dashboard]\n# @RELATION CALLS -> [_resolve_dashboard_title]\ndef _process_dashboards_for_start(\n dashboard_ids: list[int],\n event_id: str,\n environment_id: str,\n template: str,\n timezone_str: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[MaintenanceDashboardBanner]]:\n successful_dashboards: list[dict[str, Any]] = []\n failed_dashboards: list[dict[str, Any]] = []\n banners_created: list[MaintenanceDashboardBanner] = []\n\n for dash_id in dashboard_ids:\n app_logger.reason(\n \"Processing dashboard\",\n extra={\"dashboard_id\": dash_id, \"event_id\": event_id},\n )\n try:\n # Ensure banner chart exists\n banner = ensure_banner_chart(\n dash_id,\n environment_id,\n superset_client,\n db_session,\n )\n banners_created.append(banner)\n\n # Create dashboard state\n state = MaintenanceDashboardState(\n event_id=event_id,\n dashboard_id=dash_id,\n banner_id=banner.id,\n status=MaintenanceDashboardStateStatus.ACTIVE,\n )\n db_session.add(state)\n db_session.flush() # Ensure state is visible to subsequent queries\n\n # Build banner text for this dashboard's active events\n banner_text = _build_banner_text_for_dashboard(\n banner.id, db_session, template, timezone_str\n )\n\n # Update banner content on dashboard (native MARKDOWN in layout)\n if banner.chart_id:\n try:\n superset_client.update_banner_on_dashboard(\n dash_id, banner.chart_id, banner_text\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to update banner on dashboard\",\n extra={\n \"chart_id\": banner.chart_id,\n \"dashboard_id\": dash_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.APPLY_FAILED\n failed_dashboards.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Update banner text in DB\n banner.banner_text = banner_text\n db_session.flush()\n\n # Resolve dashboard title\n title = _resolve_dashboard_title(dash_id, superset_client)\n successful_dashboards.append(\n {\n \"id\": dash_id,\n \"title\": title,\n \"chart_id\": banner.chart_id,\n }\n )\n app_logger.reflect(\n \"Dashboard processed successfully\",\n extra={\"dashboard_id\": dash_id},\n )\n\n except Exception as e:\n app_logger.explore(\n \"Failed to process dashboard\",\n extra={\"dashboard_id\": dash_id},\n error=str(e),\n )\n failed_dashboards.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n\n return successful_dashboards, failed_dashboards, banners_created\n# #endregion _process_dashboards_for_start\n\n\n# #region _process_states_for_end [C:3] [TYPE Function]\n# @BRIEF Process each dashboard state for an end maintenance event: if other events\n# still use same banner → rebuild; if no other events → remove banner (chart + layout).\n# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.\n# @RELATION CALLS -> [rebuild_banner]\ndef _process_states_for_end(\n states: list[MaintenanceDashboardState],\n event_id: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> tuple[int, list[dict[str, Any]]]:\n removed_count = 0\n failed_removals: list[dict[str, Any]] = []\n\n for state in states:\n dash_id = state.dashboard_id\n app_logger.reason(\n \"Processing dashboard state for end\",\n extra={\"dashboard_id\": dash_id, \"state_id\": state.id},\n )\n\n try:\n banner_id = state.banner_id\n if not banner_id:\n # No banner linked, just mark as removed\n state.status = MaintenanceDashboardStateStatus.REMOVED\n removed_count += 1\n continue\n\n # Check if other active events still use this banner\n other_active = db_session.query(MaintenanceDashboardState).filter(\n MaintenanceDashboardState.banner_id == banner_id,\n MaintenanceDashboardState.status == MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardState.event_id != event_id,\n ).count()\n\n banner = db_session.query(MaintenanceDashboardBanner).filter(\n MaintenanceDashboardBanner.id == banner_id\n ).first()\n\n if other_active > 0 and banner:\n # Other events still active — rebuild banner text\n state.status = MaintenanceDashboardStateStatus.REMOVED\n db_session.flush()\n rebuild_banner(banner_id, db_session, superset_client)\n app_logger.reflect(\n \"Banner rebuilt after removing event\",\n extra={\"banner_id\": banner_id, \"dashboard_id\": dash_id},\n )\n elif banner:\n # No other events — remove banner entirely\n state.status = MaintenanceDashboardStateStatus.REMOVING\n db_session.flush()\n\n # Remove chart from layout\n if banner.chart_id:\n try:\n superset_client.remove_chart_from_layout(\n dash_id, banner.chart_id\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to remove chart from layout\",\n extra={\n \"dashboard_id\": dash_id,\n \"chart_id\": banner.chart_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Delete chart\n try:\n superset_client.delete_chart(banner.chart_id)\n except Exception as e:\n app_logger.explore(\n \"Failed to delete chart\",\n extra={\n \"chart_id\": banner.chart_id,\n \"dashboard_id\": dash_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Mark banner as removed\n banner.status = MaintenanceDashboardBannerStatus.REMOVED\n state.status = MaintenanceDashboardStateStatus.REMOVED\n app_logger.reflect(\n \"Banner fully removed\",\n extra={\"banner_id\": banner_id, \"dashboard_id\": dash_id},\n )\n else:\n state.status = MaintenanceDashboardStateStatus.REMOVED\n\n removed_count += 1\n\n except Exception as e:\n app_logger.explore(\n \"Failed to end maintenance for dashboard\",\n extra={\"dashboard_id\": dash_id},\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n\n return removed_count, failed_removals\n# #endregion _process_states_for_end\n\n# #endregion MaintenanceChartManager\n" + "body": "# #region MaintenanceChartManager [C:3] [TYPE Module] [SEMANTICS maintenance, chart, banner, lifecycle]\n# @BRIEF Chart operations for maintenance banners: create/get banner charts, process\n# per-dashboard state transitions for start and end orchestrators.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n)\nfrom ._banner_renderer import _build_banner_text_for_dashboard, rebuild_banner\nfrom ._dashboard_scanner import _resolve_dashboard_title\n\n\n# #region ensure_banner_chart [C:3] [TYPE Function] [SEMANTICS maintenance, banner, chart, creation]\n# @BRIEF Get or create a MaintenanceDashboardBanner for a dashboard in the target environment.\n# If an active banner exists in DB, return it. Otherwise, create a markdown chart in Superset,\n# update dashboard layout, and store the banner row.\n# @PRE dashboard_id exists in Superset. superset_client has write access.\n# @POST Returns MaintenanceDashboardBanner with chart_id set. Chart is placed at top of dashboard.\n# @SIDE_EFFECT Creates chart in Superset, modifies dashboard layout, writes DB row.\n# @RELATION DEPENDS_ON -> [SupersetDashboardsWriteMixin]\ndef ensure_banner_chart(\n dashboard_id: int,\n environment_id: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> MaintenanceDashboardBanner:\n with belief_scope(\n \"ensure_banner_chart\",\n f\"dashboard_id={dashboard_id}\",\n ):\n app_logger.reason(\n \"Ensuring banner chart exists\",\n extra={\"dashboard_id\": dashboard_id},\n )\n\n # Check for existing active banner\n existing = (\n db_session.query(MaintenanceDashboardBanner)\n .filter(\n MaintenanceDashboardBanner.dashboard_id == dashboard_id,\n MaintenanceDashboardBanner.environment_id == environment_id,\n MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE,\n )\n .first()\n )\n if existing:\n app_logger.reason(\n \"Found existing active banner, verifying chart in Superset\",\n extra={\"banner_id\": existing.id, \"chart_id\": existing.chart_id},\n )\n # Verify the chart still exists in Superset\n try:\n superset_client.get_chart(existing.chart_id)\n app_logger.reflect(\n \"Existing banner chart is alive, reusing\",\n extra={\"banner_id\": existing.id, \"chart_id\": existing.chart_id},\n )\n return existing\n except Exception:\n app_logger.explore(\n \"Existing banner chart is stale (not found in Superset), marking as REMOVED\",\n extra={\n \"banner_id\": existing.id,\n \"chart_id\": existing.chart_id,\n },\n )\n existing.status = MaintenanceDashboardBannerStatus.REMOVED\n db_session.flush()\n # Fall through to create a new chart\n\n # Create markdown chart in Superset\n try:\n chart_id = superset_client.create_markdown_chart(\n dashboard_id, \"*Maintenance banner placeholder*\"\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to create markdown chart\",\n extra={\"dashboard_id\": dashboard_id},\n error=str(e),\n )\n raise\n\n # Insert chart at top of dashboard layout\n try:\n superset_client.update_dashboard_layout(dashboard_id, chart_id)\n except Exception as e:\n app_logger.explore(\n \"Failed to update dashboard layout, deleting chart\",\n extra={\"dashboard_id\": dashboard_id, \"chart_id\": chart_id},\n error=str(e),\n )\n # Clean up the chart we just created\n try:\n superset_client.delete_chart(chart_id)\n except Exception:\n pass\n raise\n\n # Persist banner row\n banner = MaintenanceDashboardBanner(\n environment_id=environment_id,\n dashboard_id=dashboard_id,\n chart_id=chart_id,\n banner_text=\"\",\n status=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n db_session.add(banner)\n db_session.flush()\n\n app_logger.reflect(\n \"Banner chart created and persisted\",\n extra={\n \"banner_id\": banner.id,\n \"chart_id\": chart_id,\n \"dashboard_id\": dashboard_id,\n },\n )\n return banner\n# #endregion ensure_banner_chart\n\n\n# #region _process_dashboards_for_start [C:3] [TYPE Function]\n# @BRIEF Process each dashboard for a start maintenance event: ensure banner chart,\n# create dashboard state, build banner text, update chart markdown.\n# @SIDE_EFFECT Creates/updates Superset charts, writes DB rows.\n# @RELATION CALLS -> [ensure_banner_chart]\n# @RELATION CALLS -> [_build_banner_text_for_dashboard]\n# @RELATION CALLS -> [_resolve_dashboard_title]\ndef _process_dashboards_for_start(\n dashboard_ids: list[int],\n event_id: str,\n environment_id: str,\n template: str,\n timezone_str: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[MaintenanceDashboardBanner]]:\n successful_dashboards: list[dict[str, Any]] = []\n failed_dashboards: list[dict[str, Any]] = []\n banners_created: list[MaintenanceDashboardBanner] = []\n\n for dash_id in dashboard_ids:\n app_logger.reason(\n \"Processing dashboard\",\n extra={\"dashboard_id\": dash_id, \"event_id\": event_id},\n )\n try:\n # Ensure banner chart exists\n banner = ensure_banner_chart(\n dash_id,\n environment_id,\n superset_client,\n db_session,\n )\n banners_created.append(banner)\n\n # Create dashboard state\n state = MaintenanceDashboardState(\n event_id=event_id,\n dashboard_id=dash_id,\n banner_id=banner.id,\n status=MaintenanceDashboardStateStatus.ACTIVE,\n )\n db_session.add(state)\n db_session.flush() # Ensure state is visible to subsequent queries\n\n # Build banner text for this dashboard's active events\n banner_text = _build_banner_text_for_dashboard(\n banner.id, db_session, template, timezone_str\n )\n\n # Update banner content on dashboard (native MARKDOWN in layout)\n if banner.chart_id:\n try:\n superset_client.update_banner_on_dashboard(\n dash_id, banner.chart_id, banner_text\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to update banner on dashboard\",\n extra={\n \"chart_id\": banner.chart_id,\n \"dashboard_id\": dash_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.APPLY_FAILED\n failed_dashboards.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Update banner text in DB\n banner.banner_text = banner_text\n db_session.flush()\n\n # Resolve dashboard title\n title = _resolve_dashboard_title(dash_id, superset_client)\n successful_dashboards.append(\n {\n \"id\": dash_id,\n \"title\": title,\n \"chart_id\": banner.chart_id,\n }\n )\n app_logger.reflect(\n \"Dashboard processed successfully\",\n extra={\"dashboard_id\": dash_id},\n )\n\n except Exception as e:\n app_logger.explore(\n \"Failed to process dashboard\",\n extra={\"dashboard_id\": dash_id},\n error=str(e),\n )\n failed_dashboards.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n\n return successful_dashboards, failed_dashboards, banners_created\n# #endregion _process_dashboards_for_start\n\n\n# #region _process_states_for_end [C:3] [TYPE Function]\n# @BRIEF Process each dashboard state for an end maintenance event: if other events\n# still use same banner → rebuild; if no other events → remove banner (chart + layout).\n# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.\n# @RELATION CALLS -> [rebuild_banner]\ndef _process_states_for_end(\n states: list[MaintenanceDashboardState],\n event_id: str,\n superset_client: SupersetClient,\n db_session: Session,\n) -> tuple[int, list[dict[str, Any]]]:\n removed_count = 0\n failed_removals: list[dict[str, Any]] = []\n\n for state in states:\n dash_id = state.dashboard_id\n app_logger.reason(\n \"Processing dashboard state for end\",\n extra={\"dashboard_id\": dash_id, \"state_id\": state.id},\n )\n\n try:\n banner_id = state.banner_id\n if not banner_id:\n # No banner linked, just mark as removed\n state.status = MaintenanceDashboardStateStatus.REMOVED\n removed_count += 1\n continue\n\n # Check if other active events still use this banner\n other_active = db_session.query(MaintenanceDashboardState).filter(\n MaintenanceDashboardState.banner_id == banner_id,\n MaintenanceDashboardState.status == MaintenanceDashboardStateStatus.ACTIVE,\n MaintenanceDashboardState.event_id != event_id,\n ).count()\n\n banner = db_session.query(MaintenanceDashboardBanner).filter(\n MaintenanceDashboardBanner.id == banner_id\n ).first()\n\n if other_active > 0 and banner:\n # Other events still active — rebuild banner text\n state.status = MaintenanceDashboardStateStatus.REMOVED\n db_session.flush()\n rebuild_banner(banner_id, db_session, superset_client)\n app_logger.reflect(\n \"Banner rebuilt after removing event\",\n extra={\"banner_id\": banner_id, \"dashboard_id\": dash_id},\n )\n elif banner:\n # No other events — remove banner entirely\n state.status = MaintenanceDashboardStateStatus.REMOVING\n db_session.flush()\n\n # Remove chart from layout\n if banner.chart_id:\n try:\n superset_client.remove_chart_from_layout(\n dash_id, banner.chart_id\n )\n except Exception as e:\n app_logger.explore(\n \"Failed to remove chart from layout\",\n extra={\n \"dashboard_id\": dash_id,\n \"chart_id\": banner.chart_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Delete chart\n try:\n superset_client.delete_chart(banner.chart_id)\n except Exception as e:\n app_logger.explore(\n \"Failed to delete chart\",\n extra={\n \"chart_id\": banner.chart_id,\n \"dashboard_id\": dash_id,\n },\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n continue\n\n # Mark banner as removed\n banner.status = MaintenanceDashboardBannerStatus.REMOVED\n state.status = MaintenanceDashboardStateStatus.REMOVED\n app_logger.reflect(\n \"Banner fully removed\",\n extra={\"banner_id\": banner_id, \"dashboard_id\": dash_id},\n )\n else:\n state.status = MaintenanceDashboardStateStatus.REMOVED\n\n removed_count += 1\n\n except Exception as e:\n app_logger.explore(\n \"Failed to end maintenance for dashboard\",\n extra={\"dashboard_id\": dash_id},\n error=str(e),\n )\n state.status = MaintenanceDashboardStateStatus.REMOVAL_FAILED\n failed_removals.append(\n {\"id\": dash_id, \"title\": str(dash_id), \"error\": str(e)}\n )\n\n return removed_count, failed_removals\n# #endregion _process_states_for_end\n\n# #endregion MaintenanceChartManager\n" }, { "contract_id": "ensure_banner_chart", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_chart_manager.py", - "start_line": 27, - "end_line": 134, + "start_line": 24, + "end_line": 131, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47078,8 +47958,8 @@ "contract_id": "_process_dashboards_for_start", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_chart_manager.py", - "start_line": 137, - "end_line": 237, + "start_line": 134, + "end_line": 234, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47096,8 +47976,8 @@ "contract_id": "_process_states_for_end", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_chart_manager.py", - "start_line": 240, - "end_line": 357, + "start_line": 237, + "end_line": 354, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47115,7 +47995,7 @@ "contract_type": "Module", "file_path": "backend/src/services/maintenance/_dashboard_scanner.py", "start_line": 1, - "end_line": 205, + "end_line": 203, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47126,14 +48006,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceDashboardScanner [C:3] [TYPE Module] [SEMANTICS maintenance, dashboard, scanner, tables]\n# @BRIEF Dashboard discovery and filtering helpers for maintenance banner feature.\n# Scan Superset datasets, match tables, apply scope/excluded/forced filters.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SqlTableExtractorModule]\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import DashboardScope, MaintenanceSettings\nfrom ..sql_table_extractor import extract_tables_from_sql\n\n\n# #region find_affected_dashboards [C:3] [TYPE Function] [SEMANTICS maintenance, dashboard, discovery, tables]\n# @BRIEF Find all dashboard IDs whose datasets reference any of the given tables.\n# Supports both table-based (schema, table_name) and virtual SQL datasets (via SqlTableExtractor).\n# Applies scope/excluded/forced filtering from MaintenanceSettings.\n# @PRE tables is a non-empty list of \"schema.table\" strings. superset_client is authenticated.\n# @POST Returns a deduplicated list of dashboard IDs that should receive banners.\n# @SIDE_EFFECT Fetches all datasets from Superset (paginated). May be slow with many datasets.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SqlTableExtractorModule]\ndef find_affected_dashboards(\n tables: list[str],\n superset_client: SupersetClient,\n settings: MaintenanceSettings | None = None,\n) -> list[int]:\n with belief_scope(\n \"find_affected_dashboards\",\n f\"tables={tables}\",\n ):\n app_logger.reason(\n \"Finding affected dashboards\",\n extra={\"tables\": tables},\n )\n\n if not tables:\n app_logger.reflect(\"No tables provided, returning empty\")\n return []\n\n # Normalize input tables to lowercase set for matching\n target_tables = {t.lower().strip() for t in tables if t.strip()}\n if not target_tables:\n return []\n\n # Fetch all datasets from Superset\n try:\n _, datasets = superset_client.get_datasets()\n except Exception as e:\n app_logger.explore(\n \"Failed to fetch datasets from Superset\",\n extra={},\n error=str(e),\n )\n raise\n\n app_logger.reason(\n f\"Scanning {len(datasets)} datasets for table references\",\n extra={\"target_tables\": list(target_tables)},\n )\n\n matched_dashboard_ids: set[int] = set()\n\n for ds in datasets:\n ds_schema = (ds.get(\"schema\") or \"\").lower().strip()\n ds_table = (ds.get(\"table_name\") or \"\").lower().strip()\n is_virtual = ds.get(\"is_sqllab_view\", False) or ds.get(\"sql\", \"\") not in (\n None,\n \"\",\n )\n ds_sql = ds.get(\"sql\") or \"\"\n ds_id = ds.get(\"id\")\n\n # Phase 1: Table-based match\n if not is_virtual and ds_schema and ds_table:\n qualified = f\"{ds_schema}.{ds_table}\"\n if qualified in target_tables:\n linked = _get_linked_dashboards(superset_client, ds_id)\n matched_dashboard_ids.update(linked)\n continue\n\n # Phase 2: Virtual/SQL dataset — extract tables from SQL text\n if is_virtual and ds_sql:\n extracted = extract_tables_from_sql(ds_sql)\n if extracted & target_tables:\n linked = _get_linked_dashboards(superset_client, ds_id)\n matched_dashboard_ids.update(linked)\n\n app_logger.reflect(\n f\"Found {len(matched_dashboard_ids)} affected dashboards\",\n extra={\"dashboard_ids\": list(matched_dashboard_ids)},\n )\n\n # Apply filtering from settings\n result = _apply_dashboard_filters(\n list(matched_dashboard_ids), superset_client, settings\n )\n\n app_logger.reflect(\n f\"After filtering: {len(result)} dashboards\",\n extra={\"dashboard_ids\": result},\n )\n return result\n# #endregion find_affected_dashboards\n\n\n# #region _get_linked_dashboards [C:2] [TYPE Function]\n# @BRIEF Fetch linked dashboards for a dataset from Superset.\n# @SIDE_EFFECT Calls Superset API.\ndef _get_linked_dashboards(\n superset_client: SupersetClient,\n dataset_id: int | None,\n) -> set[int]:\n if not dataset_id:\n return set()\n try:\n detail = superset_client.get_dataset_detail(dataset_id)\n linked = detail.get(\"linked_dashboards\", [])\n return {int(d[\"id\"]) for d in linked if d.get(\"id\")}\n except Exception as e:\n app_logger.explore(\n \"Failed to fetch linked dashboards\",\n extra={\"dataset_id\": dataset_id},\n error=str(e),\n )\n return set()\n# #endregion _get_linked_dashboards\n\n\n# #region _apply_dashboard_filters [C:2] [TYPE Function]\n# @BRIEF Apply scope (published/draft/all), excluded, and forced filters from settings.\ndef _apply_dashboard_filters(\n dashboard_ids: list[int],\n superset_client: SupersetClient,\n settings: MaintenanceSettings | None,\n) -> list[int]:\n if not settings:\n return dashboard_ids\n\n # If forced list exists, include those unconditionally\n forced_ids = set(settings.forced_dashboard_ids or [])\n excluded_ids = set(settings.excluded_dashboard_ids or [])\n\n # Apply scope filtering\n if settings.dashboard_scope != DashboardScope.ALL:\n try:\n _, all_dashboards = superset_client.get_dashboards()\n scope = settings.dashboard_scope\n filtered: list[int] = []\n for did in dashboard_ids:\n dash = next(\n (d for d in all_dashboards if d.get(\"id\") == did), None\n )\n if dash is None:\n continue\n is_published = dash.get(\"published\", True)\n if scope == DashboardScope.PUBLISHED_ONLY and not is_published:\n continue\n if scope == DashboardScope.DRAFT_ONLY and is_published:\n continue\n filtered.append(did)\n dashboard_ids = filtered\n except Exception as e:\n app_logger.explore(\n \"Scope filtering failed, proceeding without it\",\n extra={},\n error=str(e),\n )\n\n # Apply excluded (forced overrides excluded per FR-009-b)\n result = [\n did for did in dashboard_ids if did not in excluded_ids or did in forced_ids\n ]\n\n # Add forced dashboards that are not already included\n for fid in forced_ids:\n if fid not in result:\n result.append(fid)\n\n return result\n# #endregion _apply_dashboard_filters\n\n\n# #region _resolve_dashboard_title [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard title from Superset by ID.\ndef _resolve_dashboard_title(\n dashboard_id: int,\n superset_client: SupersetClient,\n) -> str:\n try:\n _, dashboards = superset_client.get_dashboards()\n for d in dashboards:\n if d.get(\"id\") == dashboard_id:\n return d.get(\"dashboard_title\") or d.get(\"title\") or str(dashboard_id)\n except Exception:\n pass\n return str(dashboard_id)\n# #endregion _resolve_dashboard_title\n\n# #endregion MaintenanceDashboardScanner\n" + "body": "# #region MaintenanceDashboardScanner [C:3] [TYPE Module] [SEMANTICS maintenance, dashboard, scanner, tables]\n# @BRIEF Dashboard discovery and filtering helpers for maintenance banner feature.\n# Scan Superset datasets, match tables, apply scope/excluded/forced filters.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SqlTableExtractorModule]\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n\n\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import DashboardScope, MaintenanceSettings\nfrom ..sql_table_extractor import extract_tables_from_sql\n\n\n# #region find_affected_dashboards [C:3] [TYPE Function] [SEMANTICS maintenance, dashboard, discovery, tables]\n# @BRIEF Find all dashboard IDs whose datasets reference any of the given tables.\n# Supports both table-based (schema, table_name) and virtual SQL datasets (via SqlTableExtractor).\n# Applies scope/excluded/forced filtering from MaintenanceSettings.\n# @PRE tables is a non-empty list of \"schema.table\" strings. superset_client is authenticated.\n# @POST Returns a deduplicated list of dashboard IDs that should receive banners.\n# @SIDE_EFFECT Fetches all datasets from Superset (paginated). May be slow with many datasets.\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [SqlTableExtractorModule]\ndef find_affected_dashboards(\n tables: list[str],\n superset_client: SupersetClient,\n settings: MaintenanceSettings | None = None,\n) -> list[int]:\n with belief_scope(\n \"find_affected_dashboards\",\n f\"tables={tables}\",\n ):\n app_logger.reason(\n \"Finding affected dashboards\",\n extra={\"tables\": tables},\n )\n\n if not tables:\n app_logger.reflect(\"No tables provided, returning empty\")\n return []\n\n # Normalize input tables to lowercase set for matching\n target_tables = {t.lower().strip() for t in tables if t.strip()}\n if not target_tables:\n return []\n\n # Fetch all datasets from Superset\n try:\n _, datasets = superset_client.get_datasets()\n except Exception as e:\n app_logger.explore(\n \"Failed to fetch datasets from Superset\",\n extra={},\n error=str(e),\n )\n raise\n\n app_logger.reason(\n f\"Scanning {len(datasets)} datasets for table references\",\n extra={\"target_tables\": list(target_tables)},\n )\n\n matched_dashboard_ids: set[int] = set()\n\n for ds in datasets:\n ds_schema = (ds.get(\"schema\") or \"\").lower().strip()\n ds_table = (ds.get(\"table_name\") or \"\").lower().strip()\n is_virtual = ds.get(\"is_sqllab_view\", False) or ds.get(\"sql\", \"\") not in (\n None,\n \"\",\n )\n ds_sql = ds.get(\"sql\") or \"\"\n ds_id = ds.get(\"id\")\n\n # Phase 1: Table-based match\n if not is_virtual and ds_schema and ds_table:\n qualified = f\"{ds_schema}.{ds_table}\"\n if qualified in target_tables:\n linked = _get_linked_dashboards(superset_client, ds_id)\n matched_dashboard_ids.update(linked)\n continue\n\n # Phase 2: Virtual/SQL dataset — extract tables from SQL text\n if is_virtual and ds_sql:\n extracted = extract_tables_from_sql(ds_sql)\n if extracted & target_tables:\n linked = _get_linked_dashboards(superset_client, ds_id)\n matched_dashboard_ids.update(linked)\n\n app_logger.reflect(\n f\"Found {len(matched_dashboard_ids)} affected dashboards\",\n extra={\"dashboard_ids\": list(matched_dashboard_ids)},\n )\n\n # Apply filtering from settings\n result = _apply_dashboard_filters(\n list(matched_dashboard_ids), superset_client, settings\n )\n\n app_logger.reflect(\n f\"After filtering: {len(result)} dashboards\",\n extra={\"dashboard_ids\": result},\n )\n return result\n# #endregion find_affected_dashboards\n\n\n# #region _get_linked_dashboards [C:2] [TYPE Function]\n# @BRIEF Fetch linked dashboards for a dataset from Superset.\n# @SIDE_EFFECT Calls Superset API.\ndef _get_linked_dashboards(\n superset_client: SupersetClient,\n dataset_id: int | None,\n) -> set[int]:\n if not dataset_id:\n return set()\n try:\n detail = superset_client.get_dataset_detail(dataset_id)\n linked = detail.get(\"linked_dashboards\", [])\n return {int(d[\"id\"]) for d in linked if d.get(\"id\")}\n except Exception as e:\n app_logger.explore(\n \"Failed to fetch linked dashboards\",\n extra={\"dataset_id\": dataset_id},\n error=str(e),\n )\n return set()\n# #endregion _get_linked_dashboards\n\n\n# #region _apply_dashboard_filters [C:2] [TYPE Function]\n# @BRIEF Apply scope (published/draft/all), excluded, and forced filters from settings.\ndef _apply_dashboard_filters(\n dashboard_ids: list[int],\n superset_client: SupersetClient,\n settings: MaintenanceSettings | None,\n) -> list[int]:\n if not settings:\n return dashboard_ids\n\n # If forced list exists, include those unconditionally\n forced_ids = set(settings.forced_dashboard_ids or [])\n excluded_ids = set(settings.excluded_dashboard_ids or [])\n\n # Apply scope filtering\n if settings.dashboard_scope != DashboardScope.ALL:\n try:\n _, all_dashboards = superset_client.get_dashboards()\n scope = settings.dashboard_scope\n filtered: list[int] = []\n for did in dashboard_ids:\n dash = next(\n (d for d in all_dashboards if d.get(\"id\") == did), None\n )\n if dash is None:\n continue\n is_published = dash.get(\"published\", True)\n if scope == DashboardScope.PUBLISHED_ONLY and not is_published:\n continue\n if scope == DashboardScope.DRAFT_ONLY and is_published:\n continue\n filtered.append(did)\n dashboard_ids = filtered\n except Exception as e:\n app_logger.explore(\n \"Scope filtering failed, proceeding without it\",\n extra={},\n error=str(e),\n )\n\n # Apply excluded (forced overrides excluded per FR-009-b)\n result = [\n did for did in dashboard_ids if did not in excluded_ids or did in forced_ids\n ]\n\n # Add forced dashboards that are not already included\n for fid in forced_ids:\n if fid not in result:\n result.append(fid)\n\n return result\n# #endregion _apply_dashboard_filters\n\n\n# #region _resolve_dashboard_title [C:2] [TYPE Function]\n# @BRIEF Resolve dashboard title from Superset by ID.\ndef _resolve_dashboard_title(\n dashboard_id: int,\n superset_client: SupersetClient,\n) -> str:\n try:\n _, dashboards = superset_client.get_dashboards()\n for d in dashboards:\n if d.get(\"id\") == dashboard_id:\n return d.get(\"dashboard_title\") or d.get(\"title\") or str(dashboard_id)\n except Exception:\n pass\n return str(dashboard_id)\n# #endregion _resolve_dashboard_title\n\n# #endregion MaintenanceDashboardScanner\n" }, { "contract_id": "find_affected_dashboards", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_dashboard_scanner.py", - "start_line": 19, - "end_line": 109, + "start_line": 17, + "end_line": 107, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -47150,8 +48030,8 @@ "contract_id": "_get_linked_dashboards", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_dashboard_scanner.py", - "start_line": 112, - "end_line": 132, + "start_line": 110, + "end_line": 130, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -47169,8 +48049,8 @@ "contract_id": "_apply_dashboard_filters", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_dashboard_scanner.py", - "start_line": 135, - "end_line": 186, + "start_line": 133, + "end_line": 184, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -47187,8 +48067,8 @@ "contract_id": "_resolve_dashboard_title", "contract_type": "Function", "file_path": "backend/src/services/maintenance/_dashboard_scanner.py", - "start_line": 189, - "end_line": 203, + "start_line": 187, + "end_line": 201, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -47217,7 +48097,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region MaintenanceOrchestrators [C:4] [TYPE Module] [SEMANTICS maintenance, orchestrator, lifecycle]\n# @BRIEF C4 orchestrators for maintenance event lifecycle: start, end, end-all.\n# Private helpers extracted to sub-modules. All orchestrators use belief runtime.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardScanner]\n# @RELATION DEPENDS_ON -> [MaintenanceBannerRenderer]\n# @RELATION DEPENDS_ON -> [MaintenanceChartManager]\n# @INVARIANT C4 methods use belief_scope + REASON/REFLECT/EXPLORE markers.\n# @INVARIANT Idempotency key: sorted lowercased (tables, start_time, end_time). message NOT in key.\n\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardState,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom ._chart_manager import _process_dashboards_for_start, _process_states_for_end\nfrom ._dashboard_scanner import find_affected_dashboards\n\n\n# #region build_idempotency_key [C:2] [TYPE Function]\n# @BRIEF Build idempotency key from (tables, start_time, end_time). Sorted lowercased tables, ISO timestamps.\n# @PRE tables is a list of strings. start_time is a datetime or None. end_time is a datetime or None.\n# @POST Returns a tuple of (frozenset(str), str|None, str|None) for DB comparison.\n# @RATIONALE message is intentionally excluded from key per spec — different message with same tables/times = same event.\ndef build_idempotency_key(\n tables: list[str],\n start_time: datetime | None,\n end_time: datetime | None,\n) -> tuple[frozenset[str], str | None, str | None]:\n sorted_tables = sorted(t.strip().lower() for t in tables if t.strip())\n start_key = start_time.isoformat() if start_time else None\n end_key = end_time.isoformat() if end_time else None\n return (frozenset(sorted_tables), start_key, end_key)\n# #endregion build_idempotency_key\n\n\n# #region start_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, start, orchestration, async]\n# @BRIEF C4 orchestrator for starting a maintenance event. Runs inside a TaskManager task.\n# Idempotency check with SELECT ... FOR UPDATE. Creates event states, finds dashboards,\n# ensures banner charts, builds text, updates charts. Event transitions to ACTIVE or PARTIAL.\n# @PRE event_id points to a valid MaintenanceEvent with status=PENDING.\n# @POST Event and dashboard states transitioned to ACTIVE/PARTIAL/APPLY_FAILED.\n# Banner charts created and updated in Superset.\n# @SIDE_EFFECT Modifies Superset dashboards (creates/updates charts, modifies layouts).\n# Writes multiple DB rows (dashboard states, banner updates).\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [find_affected_dashboards]\n# @RELATION CALLS -> [_process_dashboards_for_start]\ndef start_maintenance(\n event_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"start_maintenance\", f\"event_id={event_id}\"):\n app_logger.reason(\n \"Starting maintenance event\",\n extra={\"event_id\": event_id},\n )\n\n # Load event\n event = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == event_id\n ).with_for_update().first()\n if not event:\n app_logger.explore(\n \"Event not found\",\n extra={\"event_id\": event_id},\n error=\"EventNotFound\",\n )\n return {\"maintenance_id\": event_id, \"status\": \"failed\", \"error\": \"Event not found\"}\n\n if event.status != MaintenanceEventStatus.PENDING:\n app_logger.reflect(\n \"Event already processed\",\n extra={\"event_id\": event_id, \"status\": event.status.value},\n )\n return {\"maintenance_id\": event_id, \"status\": event.status.value}\n\n # Transition to APPLYING\n event.status = MaintenanceEventStatus.APPLYING\n db_session.flush()\n\n # Load settings\n settings = db_session.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n environment_id = settings.target_environment_id if settings else event.environment_id\n template = settings.banner_template if settings else \"\"\n timezone_str = settings.display_timezone if settings else \"UTC\"\n\n # Find affected dashboards\n try:\n dashboard_ids = find_affected_dashboards(\n event.tables,\n superset_client,\n settings,\n )\n except Exception as e:\n app_logger.explore(\n \"Dashboard discovery failed\",\n extra={\"event_id\": event_id},\n error=str(e),\n )\n event.status = MaintenanceEventStatus.FAILED\n db_session.flush()\n return {\n \"maintenance_id\": event_id,\n \"status\": \"failed\",\n \"error\": str(e),\n \"affected_dashboards\": 0,\n \"dashboards\": [],\n \"failed_dashboards\": [],\n \"unmatched_tables\": [],\n }\n\n if not dashboard_ids:\n app_logger.reflect(\n \"No matching dashboards found\",\n extra={\"event_id\": event_id},\n )\n event.status = MaintenanceEventStatus.ACTIVE\n db_session.flush()\n return {\n \"maintenance_id\": event_id,\n \"status\": \"no_match\",\n \"affected_dashboards\": 0,\n \"dashboards\": [],\n \"failed_dashboards\": [],\n \"unmatched_tables\": [\n t for t in event.tables if t not in dashboard_ids\n ],\n }\n\n # Process each dashboard\n successful_dashboards, failed_dashboards, _ = _process_dashboards_for_start(\n dashboard_ids,\n event_id,\n environment_id,\n template,\n timezone_str,\n superset_client,\n db_session,\n )\n\n # Determine final event status\n if not failed_dashboards:\n event.status = MaintenanceEventStatus.ACTIVE\n elif successful_dashboards:\n event.status = MaintenanceEventStatus.PARTIAL\n else:\n event.status = MaintenanceEventStatus.FAILED\n\n db_session.flush()\n\n result = {\n \"maintenance_id\": event_id,\n \"status\": event.status.value,\n \"affected_dashboards\": len(successful_dashboards),\n \"dashboards\": successful_dashboards,\n \"failed_dashboards\": failed_dashboards,\n \"unmatched_tables\": [],\n }\n\n app_logger.reflect(\n \"Maintenance start completed\",\n extra={\n \"event_id\": event_id,\n \"status\": event.status.value,\n \"successful\": len(successful_dashboards),\n \"failed\": len(failed_dashboards),\n },\n )\n return result\n# #endregion start_maintenance\n\n\n# #region end_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end, orchestration, async, removal]\n# @BRIEF C4 orchestrator for ending a maintenance event. Runs inside a TaskManager task.\n# Event → ENDING. Delegates per-state processing to _process_states_for_end.\n# Event → COMPLETED when done.\n# @PRE event_id points to a valid active MaintenanceEvent.\n# @POST Event transitions to COMPLETED. Dashboard states cleaned up. Charts removed when no other events.\n# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [_process_states_for_end]\ndef end_maintenance(\n event_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"end_maintenance\", f\"event_id={event_id}\"):\n app_logger.reason(\n \"Ending maintenance event\",\n extra={\"event_id\": event_id},\n )\n\n # Load event with lock\n event = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == event_id\n ).with_for_update().first()\n if not event:\n app_logger.explore(\n \"Event not found\",\n extra={\"event_id\": event_id},\n error=\"EventNotFound\",\n )\n return {\"maintenance_id\": event_id, \"status\": \"failed\", \"error\": \"Event not found\"}\n\n # Idempotent: already completed\n if event.status == MaintenanceEventStatus.COMPLETED:\n app_logger.reflect(\n \"Event already completed, idempotent\",\n extra={\"event_id\": event_id},\n )\n return {\"maintenance_id\": event_id, \"status\": \"already_completed\", \"removed_from\": 0, \"failed_removals\": []}\n\n # Transition to ENDING\n event.status = MaintenanceEventStatus.ENDING\n db_session.flush()\n\n # Find all dashboard states for this event\n states = db_session.query(MaintenanceDashboardState).filter(\n MaintenanceDashboardState.event_id == event_id\n ).all()\n\n # Delegate per-state processing\n removed_count, failed_removals = _process_states_for_end(\n states, event_id, superset_client, db_session,\n )\n\n # Transition event to COMPLETED (even if some removals failed)\n event.status = MaintenanceEventStatus.COMPLETED\n db_session.flush()\n\n result = {\n \"maintenance_id\": event_id,\n \"status\": \"completed\" if not failed_removals else \"partial\",\n \"removed_from\": removed_count,\n \"failed_removals\": failed_removals,\n }\n\n app_logger.reflect(\n \"Maintenance end completed\",\n extra={\n \"event_id\": event_id,\n \"removed\": removed_count,\n \"failed\": len(failed_removals),\n },\n )\n return result\n# #endregion end_maintenance\n\n\n# #region end_all_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end-all, orchestration, bulk]\n# @BRIEF End ALL active maintenance events. Iterates active events, calls end_maintenance for each.\n# @PRE None.\n# @POST All events ended. All banners removed or rebuilt (last event removed).\n# @SIDE_EFFECT Modifies multiple Superset dashboards. Writes multiple DB rows.\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [end_maintenance]\ndef end_all_maintenance(\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"end_all_maintenance\"):\n app_logger.reason(\"Ending all active maintenance events\")\n\n active_events = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.APPLYING,\n ])\n ).all()\n\n if not active_events:\n app_logger.reflect(\"No active events to end\")\n return {\n \"status\": \"completed\",\n \"closed_events\": 0,\n \"removed_from\": 0,\n \"failed_removals\": [],\n }\n\n total_removed = 0\n all_failed: list[dict[str, Any]] = []\n closed_count = 0\n\n for event in active_events:\n app_logger.reason(\n \"Ending event\",\n extra={\"event_id\": event.id},\n )\n try:\n result = end_maintenance(\n event.id, db_session, superset_client\n )\n total_removed += result.get(\"removed_from\", 0)\n all_failed.extend(result.get(\"failed_removals\", []))\n closed_count += 1\n except Exception as e:\n app_logger.explore(\n \"Failed to end event\",\n extra={\"event_id\": event.id},\n error=str(e),\n )\n all_failed.append(\n {\"id\": 0, \"title\": event.id, \"error\": str(e)}\n )\n\n app_logger.reflect(\n \"All events ended\",\n extra={\n \"closed\": closed_count,\n \"removed\": total_removed,\n \"failed\": len(all_failed),\n },\n )\n\n return {\n \"status\": \"completed\" if not all_failed else \"partial\",\n \"closed_events\": closed_count,\n \"removed_from\": total_removed,\n \"failed_removals\": all_failed,\n }\n# #endregion end_all_maintenance\n\n# #endregion MaintenanceOrchestrators\n" + "body": "# #region MaintenanceOrchestrators [C:4] [TYPE Module] [SEMANTICS maintenance, orchestrator, lifecycle]\n# @BRIEF C4 orchestrators for maintenance event lifecycle: start, end, end-all.\n# Private helpers extracted to sub-modules. All orchestrators use belief runtime.\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [MaintenanceModels]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [MaintenanceDashboardScanner]\n# @RELATION DEPENDS_ON -> [MaintenanceBannerRenderer]\n# @RELATION DEPENDS_ON -> [MaintenanceChartManager]\n# @INVARIANT C4 methods use belief_scope + REASON/REFLECT/EXPLORE markers.\n# @INVARIANT Idempotency key: sorted lowercased (tables, start_time, end_time). message NOT in key.\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ...core.logger import belief_scope, logger as app_logger\nfrom ...core.superset_client import SupersetClient\nfrom ...models.maintenance import (\n MaintenanceDashboardState,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom ._chart_manager import _process_dashboards_for_start, _process_states_for_end\nfrom ._dashboard_scanner import find_affected_dashboards\n\n\n# #region build_idempotency_key [C:2] [TYPE Function]\n# @BRIEF Build idempotency key from (tables, start_time, end_time). Sorted lowercased tables, ISO timestamps.\n# @PRE tables is a list of strings. start_time is a datetime or None. end_time is a datetime or None.\n# @POST Returns a tuple of (frozenset(str), str|None, str|None) for DB comparison.\n# @RATIONALE message is intentionally excluded from key per spec — different message with same tables/times = same event.\ndef build_idempotency_key(\n tables: list[str],\n start_time: datetime | None,\n end_time: datetime | None,\n) -> tuple[frozenset[str], str | None, str | None]:\n sorted_tables = sorted(t.strip().lower() for t in tables if t.strip())\n start_key = start_time.isoformat() if start_time else None\n end_key = end_time.isoformat() if end_time else None\n return (frozenset(sorted_tables), start_key, end_key)\n# #endregion build_idempotency_key\n\n\n# #region start_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, start, orchestration, async]\n# @BRIEF C4 orchestrator for starting a maintenance event. Runs inside a TaskManager task.\n# Idempotency check with SELECT ... FOR UPDATE. Creates event states, finds dashboards,\n# ensures banner charts, builds text, updates charts. Event transitions to ACTIVE or PARTIAL.\n# @PRE event_id points to a valid MaintenanceEvent with status=PENDING.\n# @POST Event and dashboard states transitioned to ACTIVE/PARTIAL/APPLY_FAILED.\n# Banner charts created and updated in Superset.\n# @SIDE_EFFECT Modifies Superset dashboards (creates/updates charts, modifies layouts).\n# Writes multiple DB rows (dashboard states, banner updates).\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [find_affected_dashboards]\n# @RELATION CALLS -> [_process_dashboards_for_start]\ndef start_maintenance(\n event_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"start_maintenance\", f\"event_id={event_id}\"):\n app_logger.reason(\n \"Starting maintenance event\",\n extra={\"event_id\": event_id},\n )\n\n # Load event\n event = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == event_id\n ).with_for_update().first()\n if not event:\n app_logger.explore(\n \"Event not found\",\n extra={\"event_id\": event_id},\n error=\"EventNotFound\",\n )\n return {\"maintenance_id\": event_id, \"status\": \"failed\", \"error\": \"Event not found\"}\n\n if event.status != MaintenanceEventStatus.PENDING:\n app_logger.reflect(\n \"Event already processed\",\n extra={\"event_id\": event_id, \"status\": event.status.value},\n )\n return {\"maintenance_id\": event_id, \"status\": event.status.value}\n\n # Transition to APPLYING\n event.status = MaintenanceEventStatus.APPLYING\n db_session.flush()\n\n # Load settings\n settings = db_session.query(MaintenanceSettings).filter(\n MaintenanceSettings.id == \"default\"\n ).first()\n environment_id = settings.target_environment_id if settings else event.environment_id\n template = settings.banner_template if settings else \"\"\n timezone_str = settings.display_timezone if settings else \"UTC\"\n\n # Find affected dashboards\n try:\n dashboard_ids = find_affected_dashboards(\n event.tables,\n superset_client,\n settings,\n )\n except Exception as e:\n app_logger.explore(\n \"Dashboard discovery failed\",\n extra={\"event_id\": event_id},\n error=str(e),\n )\n event.status = MaintenanceEventStatus.FAILED\n db_session.flush()\n return {\n \"maintenance_id\": event_id,\n \"status\": \"failed\",\n \"error\": str(e),\n \"affected_dashboards\": 0,\n \"dashboards\": [],\n \"failed_dashboards\": [],\n \"unmatched_tables\": [],\n }\n\n if not dashboard_ids:\n app_logger.reflect(\n \"No matching dashboards found\",\n extra={\"event_id\": event_id},\n )\n event.status = MaintenanceEventStatus.ACTIVE\n db_session.flush()\n return {\n \"maintenance_id\": event_id,\n \"status\": \"no_match\",\n \"affected_dashboards\": 0,\n \"dashboards\": [],\n \"failed_dashboards\": [],\n \"unmatched_tables\": [\n t for t in event.tables if t not in dashboard_ids\n ],\n }\n\n # Process each dashboard\n successful_dashboards, failed_dashboards, _ = _process_dashboards_for_start(\n dashboard_ids,\n event_id,\n environment_id,\n template,\n timezone_str,\n superset_client,\n db_session,\n )\n\n # Determine final event status\n if not failed_dashboards:\n event.status = MaintenanceEventStatus.ACTIVE\n elif successful_dashboards:\n event.status = MaintenanceEventStatus.PARTIAL\n else:\n event.status = MaintenanceEventStatus.FAILED\n\n db_session.flush()\n\n result = {\n \"maintenance_id\": event_id,\n \"status\": event.status.value,\n \"affected_dashboards\": len(successful_dashboards),\n \"dashboards\": successful_dashboards,\n \"failed_dashboards\": failed_dashboards,\n \"unmatched_tables\": [],\n }\n\n app_logger.reflect(\n \"Maintenance start completed\",\n extra={\n \"event_id\": event_id,\n \"status\": event.status.value,\n \"successful\": len(successful_dashboards),\n \"failed\": len(failed_dashboards),\n },\n )\n return result\n# #endregion start_maintenance\n\n\n# #region end_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end, orchestration, async, removal]\n# @BRIEF C4 orchestrator for ending a maintenance event. Runs inside a TaskManager task.\n# Event → ENDING. Delegates per-state processing to _process_states_for_end.\n# Event → COMPLETED when done.\n# @PRE event_id points to a valid active MaintenanceEvent.\n# @POST Event transitions to COMPLETED. Dashboard states cleaned up. Charts removed when no other events.\n# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [_process_states_for_end]\ndef end_maintenance(\n event_id: str,\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"end_maintenance\", f\"event_id={event_id}\"):\n app_logger.reason(\n \"Ending maintenance event\",\n extra={\"event_id\": event_id},\n )\n\n # Load event with lock\n event = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == event_id\n ).with_for_update().first()\n if not event:\n app_logger.explore(\n \"Event not found\",\n extra={\"event_id\": event_id},\n error=\"EventNotFound\",\n )\n return {\"maintenance_id\": event_id, \"status\": \"failed\", \"error\": \"Event not found\"}\n\n # Idempotent: already completed\n if event.status == MaintenanceEventStatus.COMPLETED:\n app_logger.reflect(\n \"Event already completed, idempotent\",\n extra={\"event_id\": event_id},\n )\n return {\"maintenance_id\": event_id, \"status\": \"already_completed\", \"removed_from\": 0, \"failed_removals\": []}\n\n # Transition to ENDING\n event.status = MaintenanceEventStatus.ENDING\n db_session.flush()\n\n # Find all dashboard states for this event\n states = db_session.query(MaintenanceDashboardState).filter(\n MaintenanceDashboardState.event_id == event_id\n ).all()\n\n # Delegate per-state processing\n removed_count, failed_removals = _process_states_for_end(\n states, event_id, superset_client, db_session,\n )\n\n # Transition event to COMPLETED (even if some removals failed)\n event.status = MaintenanceEventStatus.COMPLETED\n db_session.flush()\n\n result = {\n \"maintenance_id\": event_id,\n \"status\": \"completed\" if not failed_removals else \"partial\",\n \"removed_from\": removed_count,\n \"failed_removals\": failed_removals,\n }\n\n app_logger.reflect(\n \"Maintenance end completed\",\n extra={\n \"event_id\": event_id,\n \"removed\": removed_count,\n \"failed\": len(failed_removals),\n },\n )\n return result\n# #endregion end_maintenance\n\n\n# #region end_all_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end-all, orchestration, bulk]\n# @BRIEF End ALL active maintenance events. Iterates active events, calls end_maintenance for each.\n# @PRE None.\n# @POST All events ended. All banners removed or rebuilt (last event removed).\n# @SIDE_EFFECT Modifies multiple Superset dashboards. Writes multiple DB rows.\n# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.\n# @RELATION CALLS -> [end_maintenance]\ndef end_all_maintenance(\n db_session: Session,\n superset_client: SupersetClient,\n) -> dict[str, Any]:\n with belief_scope(\"end_all_maintenance\"):\n app_logger.reason(\"Ending all active maintenance events\")\n\n active_events = db_session.query(MaintenanceEvent).filter(\n MaintenanceEvent.status.in_([\n MaintenanceEventStatus.ACTIVE,\n MaintenanceEventStatus.PARTIAL,\n MaintenanceEventStatus.APPLYING,\n ])\n ).all()\n\n if not active_events:\n app_logger.reflect(\"No active events to end\")\n return {\n \"status\": \"completed\",\n \"closed_events\": 0,\n \"removed_from\": 0,\n \"failed_removals\": [],\n }\n\n total_removed = 0\n all_failed: list[dict[str, Any]] = []\n closed_count = 0\n\n for event in active_events:\n app_logger.reason(\n \"Ending event\",\n extra={\"event_id\": event.id},\n )\n try:\n result = end_maintenance(\n event.id, db_session, superset_client\n )\n total_removed += result.get(\"removed_from\", 0)\n all_failed.extend(result.get(\"failed_removals\", []))\n closed_count += 1\n except Exception as e:\n app_logger.explore(\n \"Failed to end event\",\n extra={\"event_id\": event.id},\n error=str(e),\n )\n all_failed.append(\n {\"id\": 0, \"title\": event.id, \"error\": str(e)}\n )\n\n app_logger.reflect(\n \"All events ended\",\n extra={\n \"closed\": closed_count,\n \"removed\": total_removed,\n \"failed\": len(all_failed),\n },\n )\n\n return {\n \"status\": \"completed\" if not all_failed else \"partial\",\n \"closed_events\": closed_count,\n \"removed_from\": total_removed,\n \"failed_removals\": all_failed,\n }\n# #endregion end_all_maintenance\n\n# #endregion MaintenanceOrchestrators\n" }, { "contract_id": "build_idempotency_key", @@ -47634,7 +48514,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region profile_preference_service [C:4] [TYPE Module] [SEMANTICS profile,preference,crud,persistence]\n# @BRIEF Profile preference persistence — read, update, and normalize user dashboard filter preferences\n# with validation, encryption of git tokens, and deterministic default construction.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Preference CRUD is the core profile\n# operation with DB persistence, token encryption, and cross-field validation — a\n# cohesive unit that isolates cleanly from security badges and Superset lookups.\n# @REJECTED Keeping preference logic inside ProfileService was rejected because it forces all\n# three domains (preferences, security, lookup) into one class exceeding 150 lines,\n# and couples the validation logic to unrelated dependencies.\n# @PRE db session is active.\n# @POST Preference rows are created/updated with normalized values and encrypted tokens.\n# @SIDE_EFFECT Writes preference records via AuthRepository; encrypts git tokens.\n\nfrom datetime import datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import User\nfrom ..models.profile import UserDashboardPreference\nfrom ..schemas.profile import (\n ProfilePreference,\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n)\nfrom ..core.encryption import EncryptionManager\nfrom .profile_utils import (\n ProfileAuthorizationError,\n ProfileValidationError,\n build_default_preference,\n mask_secret_value,\n normalize_density,\n normalize_start_page,\n normalize_username,\n sanitize_secret,\n sanitize_text,\n sanitize_username,\n validate_update_payload,\n)\nfrom .security_badge_service import SecurityBadgeService\n\n\n# #region ProfilePreferenceService [C:4] [TYPE Class] [SEMANTICS preference,crud,validation,encryption]\n# @BRIEF Handles profile preference persistence, validation, and token encryption.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @PRE db session is active.\n# @POST Preference CRUD operations are user-scoped and return normalized ProfilePreferenceResponse.\nclass ProfilePreferenceService:\n \"\"\"Profile preference persistence and validation.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with DB session and references to shared services.\n # @PRE db session is active.\n # @POST Service is ready for preference persistence operations.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n self.auth_repository = AuthRepository(db)\n self.encryption = EncryptionManager()\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n # #endregion __init__\n\n # #region get_my_preference [TYPE Function]\n # @BRIEF Return current user's persisted preference or default non-configured view.\n # @PRE current_user is authenticated.\n # @POST Returned payload belongs to current_user only.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.get_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reflect(\"Loading current user's dashboard preference\")\n preference = self._get_preference_row(current_user.id)\n security_summary = self.security_badge_service.build_security_summary(current_user)\n\n if preference is None:\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference not configured yet\",\n preference=build_default_preference(current_user.id),\n security=security_summary,\n )\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=self._to_preference_payload(\n preference, str(current_user.id)\n ),\n security=security_summary,\n )\n # #endregion get_my_preference\n\n # #region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Return only dashboard-filter fields required by dashboards listing hot path.\n # @PRE current_user is authenticated.\n # @POST Returns normalized username and profile-default filter toggles without security summary expansion.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n with belief_scope(\n \"ProfilePreferenceService.get_dashboard_filter_binding\", f\"user_id={current_user.id}\"\n ):\n preference = self._get_preference_row(current_user.id)\n if preference is None:\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": True,\n }\n\n return {\n \"superset_username\": sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": normalize_username(\n preference.superset_username\n ),\n \"show_only_my_dashboards\": bool(preference.show_only_my_dashboards),\n \"show_only_slug_dashboards\": bool(\n preference.show_only_slug_dashboards\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n }\n # #endregion get_dashboard_filter_binding\n\n # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.utcnow()\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n\n # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.utcnow()\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n\n # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to ProfileUtils.build_default_preference.\n def _build_default_preference(self, user_id: str) -> ProfilePreference:\n return build_default_preference(user_id)\n # #endregion _build_default_preference\n\n # #region _get_preference_row [C:2] [TYPE Function]\n # @BRIEF Return persisted preference row for user or None.\n def _get_preference_row(self, user_id: str) -> UserDashboardPreference | None:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n # #endregion _get_preference_row\n\n # #region _get_or_create_preference_row [C:2] [TYPE Function]\n # @BRIEF Return existing preference row or create new unsaved row.\n def _get_or_create_preference_row(self, user_id: str) -> UserDashboardPreference:\n existing = self._get_preference_row(user_id)\n if existing is not None:\n return existing\n return UserDashboardPreference(user_id=str(user_id))\n # #endregion _get_or_create_preference_row\n# #endregion ProfilePreferenceService\n# #endregion profile_preference_service\n" + "body": "# #region profile_preference_service [C:4] [TYPE Module] [SEMANTICS profile,preference,crud,persistence]\n# @BRIEF Profile preference persistence — read, update, and normalize user dashboard filter preferences\n# with validation, encryption of git tokens, and deterministic default construction.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RATIONALE Extracted from ProfileService to satisfy INV_7. Preference CRUD is the core profile\n# operation with DB persistence, token encryption, and cross-field validation — a\n# cohesive unit that isolates cleanly from security badges and Superset lookups.\n# @REJECTED Keeping preference logic inside ProfileService was rejected because it forces all\n# three domains (preferences, security, lookup) into one class exceeding 150 lines,\n# and couples the validation logic to unrelated dependencies.\n# @PRE db session is active.\n# @POST Preference rows are created/updated with normalized values and encrypted tokens.\n# @SIDE_EFFECT Writes preference records via AuthRepository; encrypts git tokens.\n\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.auth.repository import AuthRepository\nfrom ..core.encryption import EncryptionManager\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import User\nfrom ..models.profile import UserDashboardPreference\nfrom ..schemas.profile import (\n ProfilePreference,\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n)\nfrom .profile_utils import (\n ProfileAuthorizationError,\n ProfileValidationError,\n build_default_preference,\n mask_secret_value,\n normalize_density,\n normalize_start_page,\n normalize_username,\n sanitize_secret,\n sanitize_text,\n sanitize_username,\n validate_update_payload,\n)\nfrom .security_badge_service import SecurityBadgeService\n\n\n# #region ProfilePreferenceService [C:4] [TYPE Class] [SEMANTICS preference,crud,validation,encryption]\n# @BRIEF Handles profile preference persistence, validation, and token encryption.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @PRE db session is active.\n# @POST Preference CRUD operations are user-scoped and return normalized ProfilePreferenceResponse.\nclass ProfilePreferenceService:\n \"\"\"Profile preference persistence and validation.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with DB session and references to shared services.\n # @PRE db session is active.\n # @POST Service is ready for preference persistence operations.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n self.auth_repository = AuthRepository(db)\n self.encryption = EncryptionManager()\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n # #endregion __init__\n\n # #region get_my_preference [TYPE Function]\n # @BRIEF Return current user's persisted preference or default non-configured view.\n # @PRE current_user is authenticated.\n # @POST Returned payload belongs to current_user only.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.get_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reflect(\"Loading current user's dashboard preference\")\n preference = self._get_preference_row(current_user.id)\n security_summary = self.security_badge_service.build_security_summary(current_user)\n\n if preference is None:\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference not configured yet\",\n preference=build_default_preference(current_user.id),\n security=security_summary,\n )\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=self._to_preference_payload(\n preference, str(current_user.id)\n ),\n security=security_summary,\n )\n # #endregion get_my_preference\n\n # #region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Return only dashboard-filter fields required by dashboards listing hot path.\n # @PRE current_user is authenticated.\n # @POST Returns normalized username and profile-default filter toggles without security summary expansion.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n with belief_scope(\n \"ProfilePreferenceService.get_dashboard_filter_binding\", f\"user_id={current_user.id}\"\n ):\n preference = self._get_preference_row(current_user.id)\n if preference is None:\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": True,\n }\n\n return {\n \"superset_username\": sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": normalize_username(\n preference.superset_username\n ),\n \"show_only_my_dashboards\": bool(preference.show_only_my_dashboards),\n \"show_only_slug_dashboards\": bool(\n preference.show_only_slug_dashboards\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n }\n # #endregion get_dashboard_filter_binding\n\n # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.now(UTC)\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n\n # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.now(UTC)\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n\n # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to ProfileUtils.build_default_preference.\n def _build_default_preference(self, user_id: str) -> ProfilePreference:\n return build_default_preference(user_id)\n # #endregion _build_default_preference\n\n # #region _get_preference_row [C:2] [TYPE Function]\n # @BRIEF Return persisted preference row for user or None.\n def _get_preference_row(self, user_id: str) -> UserDashboardPreference | None:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n # #endregion _get_preference_row\n\n # #region _get_or_create_preference_row [C:2] [TYPE Function]\n # @BRIEF Return existing preference row or create new unsaved row.\n def _get_or_create_preference_row(self, user_id: str) -> UserDashboardPreference:\n existing = self._get_preference_row(user_id)\n if existing is not None:\n return existing\n return UserDashboardPreference(user_id=str(user_id))\n # #endregion _get_or_create_preference_row\n# #endregion ProfilePreferenceService\n# #endregion profile_preference_service\n" }, { "contract_id": "ProfilePreferenceService", @@ -47673,7 +48553,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfilePreferenceService [C:4] [TYPE Class] [SEMANTICS preference,crud,validation,encryption]\n# @BRIEF Handles profile preference persistence, validation, and token encryption.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @PRE db session is active.\n# @POST Preference CRUD operations are user-scoped and return normalized ProfilePreferenceResponse.\nclass ProfilePreferenceService:\n \"\"\"Profile preference persistence and validation.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with DB session and references to shared services.\n # @PRE db session is active.\n # @POST Service is ready for preference persistence operations.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n self.auth_repository = AuthRepository(db)\n self.encryption = EncryptionManager()\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n # #endregion __init__\n\n # #region get_my_preference [TYPE Function]\n # @BRIEF Return current user's persisted preference or default non-configured view.\n # @PRE current_user is authenticated.\n # @POST Returned payload belongs to current_user only.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.get_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reflect(\"Loading current user's dashboard preference\")\n preference = self._get_preference_row(current_user.id)\n security_summary = self.security_badge_service.build_security_summary(current_user)\n\n if preference is None:\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference not configured yet\",\n preference=build_default_preference(current_user.id),\n security=security_summary,\n )\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=self._to_preference_payload(\n preference, str(current_user.id)\n ),\n security=security_summary,\n )\n # #endregion get_my_preference\n\n # #region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Return only dashboard-filter fields required by dashboards listing hot path.\n # @PRE current_user is authenticated.\n # @POST Returns normalized username and profile-default filter toggles without security summary expansion.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n with belief_scope(\n \"ProfilePreferenceService.get_dashboard_filter_binding\", f\"user_id={current_user.id}\"\n ):\n preference = self._get_preference_row(current_user.id)\n if preference is None:\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": True,\n }\n\n return {\n \"superset_username\": sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": normalize_username(\n preference.superset_username\n ),\n \"show_only_my_dashboards\": bool(preference.show_only_my_dashboards),\n \"show_only_slug_dashboards\": bool(\n preference.show_only_slug_dashboards\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n }\n # #endregion get_dashboard_filter_binding\n\n # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.utcnow()\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n\n # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.utcnow()\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n\n # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to ProfileUtils.build_default_preference.\n def _build_default_preference(self, user_id: str) -> ProfilePreference:\n return build_default_preference(user_id)\n # #endregion _build_default_preference\n\n # #region _get_preference_row [C:2] [TYPE Function]\n # @BRIEF Return persisted preference row for user or None.\n def _get_preference_row(self, user_id: str) -> UserDashboardPreference | None:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n # #endregion _get_preference_row\n\n # #region _get_or_create_preference_row [C:2] [TYPE Function]\n # @BRIEF Return existing preference row or create new unsaved row.\n def _get_or_create_preference_row(self, user_id: str) -> UserDashboardPreference:\n existing = self._get_preference_row(user_id)\n if existing is not None:\n return existing\n return UserDashboardPreference(user_id=str(user_id))\n # #endregion _get_or_create_preference_row\n# #endregion ProfilePreferenceService\n" + "body": "# #region ProfilePreferenceService [C:4] [TYPE Class] [SEMANTICS preference,crud,validation,encryption]\n# @BRIEF Handles profile preference persistence, validation, and token encryption.\n# @RELATION DEPENDS_ON -> [AuthRepository]\n# @RELATION DEPENDS_ON -> [EncryptionManager]\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @PRE db session is active.\n# @POST Preference CRUD operations are user-scoped and return normalized ProfilePreferenceResponse.\nclass ProfilePreferenceService:\n \"\"\"Profile preference persistence and validation.\"\"\"\n\n # #region __init__ [TYPE Function]\n # @BRIEF Initialize with DB session and references to shared services.\n # @PRE db session is active.\n # @POST Service is ready for preference persistence operations.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n self.auth_repository = AuthRepository(db)\n self.encryption = EncryptionManager()\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n # #endregion __init__\n\n # #region get_my_preference [TYPE Function]\n # @BRIEF Return current user's persisted preference or default non-configured view.\n # @PRE current_user is authenticated.\n # @POST Returned payload belongs to current_user only.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.get_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reflect(\"Loading current user's dashboard preference\")\n preference = self._get_preference_row(current_user.id)\n security_summary = self.security_badge_service.build_security_summary(current_user)\n\n if preference is None:\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference not configured yet\",\n preference=build_default_preference(current_user.id),\n security=security_summary,\n )\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference loaded\",\n preference=self._to_preference_payload(\n preference, str(current_user.id)\n ),\n security=security_summary,\n )\n # #endregion get_my_preference\n\n # #region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Return only dashboard-filter fields required by dashboards listing hot path.\n # @PRE current_user is authenticated.\n # @POST Returns normalized username and profile-default filter toggles without security summary expansion.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n with belief_scope(\n \"ProfilePreferenceService.get_dashboard_filter_binding\", f\"user_id={current_user.id}\"\n ):\n preference = self._get_preference_row(current_user.id)\n if preference is None:\n return {\n \"superset_username\": None,\n \"superset_username_normalized\": None,\n \"show_only_my_dashboards\": False,\n \"show_only_slug_dashboards\": True,\n }\n\n return {\n \"superset_username\": sanitize_username(\n preference.superset_username\n ),\n \"superset_username_normalized\": normalize_username(\n preference.superset_username\n ),\n \"show_only_my_dashboards\": bool(preference.show_only_my_dashboards),\n \"show_only_slug_dashboards\": bool(\n preference.show_only_slug_dashboards\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n }\n # #endregion get_dashboard_filter_binding\n\n # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.now(UTC)\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n\n # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.now(UTC)\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n\n # #region _build_default_preference [C:1] [TYPE Function]\n # @BRIEF Delegate to ProfileUtils.build_default_preference.\n def _build_default_preference(self, user_id: str) -> ProfilePreference:\n return build_default_preference(user_id)\n # #endregion _build_default_preference\n\n # #region _get_preference_row [C:2] [TYPE Function]\n # @BRIEF Return persisted preference row for user or None.\n def _get_preference_row(self, user_id: str) -> UserDashboardPreference | None:\n return self.auth_repository.get_user_dashboard_preference(str(user_id))\n # #endregion _get_preference_row\n\n # #region _get_or_create_preference_row [C:2] [TYPE Function]\n # @BRIEF Return existing preference row or create new unsaved row.\n def _get_or_create_preference_row(self, user_id: str) -> UserDashboardPreference:\n existing = self._get_preference_row(user_id)\n if existing is not None:\n return existing\n return UserDashboardPreference(user_id=str(user_id))\n # #endregion _get_or_create_preference_row\n# #endregion ProfilePreferenceService\n" }, { "contract_id": "get_my_preference", @@ -47730,7 +48610,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.utcnow()\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n" + "body": " # #region update_my_preference [TYPE Function]\n # @BRIEF Validate and persist current user's profile preference in self-scoped mode.\n # @PRE current_user is authenticated and payload is provided.\n # @POST Preference row for current_user is created/updated when validation passes.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n with belief_scope(\n \"ProfilePreferenceService.update_my_preference\", f\"user_id={current_user.id}\"\n ):\n logger.reason(\"Evaluating self-scope guard before preference mutation\")\n requested_user_id = str(target_user_id or current_user.id)\n if requested_user_id != str(current_user.id):\n logger.explore(\"Cross-user mutation attempt blocked\")\n raise ProfileAuthorizationError(\n \"Cross-user preference mutation is forbidden\"\n )\n\n preference = self._get_or_create_preference_row(current_user.id)\n provided_fields = set(getattr(payload, \"model_fields_set\", set()))\n\n effective_superset_username = sanitize_username(\n preference.superset_username\n )\n if \"superset_username\" in provided_fields:\n effective_superset_username = sanitize_username(\n payload.superset_username\n )\n\n effective_show_only = bool(preference.show_only_my_dashboards)\n if \"show_only_my_dashboards\" in provided_fields:\n effective_show_only = bool(payload.show_only_my_dashboards)\n\n effective_show_only_slug = (\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n )\n if \"show_only_slug_dashboards\" in provided_fields:\n effective_show_only_slug = bool(payload.show_only_slug_dashboards)\n\n effective_git_username = sanitize_text(preference.git_username)\n if \"git_username\" in provided_fields:\n effective_git_username = sanitize_text(payload.git_username)\n\n effective_git_email = sanitize_text(preference.git_email)\n if \"git_email\" in provided_fields:\n effective_git_email = sanitize_text(payload.git_email)\n\n effective_start_page = normalize_start_page(preference.start_page)\n if \"start_page\" in provided_fields:\n effective_start_page = normalize_start_page(payload.start_page)\n\n effective_auto_open_task_drawer = (\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n )\n if \"auto_open_task_drawer\" in provided_fields:\n effective_auto_open_task_drawer = bool(payload.auto_open_task_drawer)\n\n effective_dashboards_table_density = normalize_density(\n preference.dashboards_table_density\n )\n if \"dashboards_table_density\" in provided_fields:\n effective_dashboards_table_density = normalize_density(\n payload.dashboards_table_density\n )\n\n effective_telegram_id = sanitize_text(preference.telegram_id)\n if \"telegram_id\" in provided_fields:\n effective_telegram_id = sanitize_text(payload.telegram_id)\n\n effective_email_address = sanitize_text(preference.email_address)\n if \"email_address\" in provided_fields:\n effective_email_address = sanitize_text(payload.email_address)\n\n effective_notify_on_fail = (\n bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True\n )\n if \"notify_on_fail\" in provided_fields:\n effective_notify_on_fail = bool(payload.notify_on_fail)\n\n validation_errors = validate_update_payload(\n superset_username=effective_superset_username,\n show_only_my_dashboards=effective_show_only,\n git_email=effective_git_email,\n start_page=effective_start_page,\n dashboards_table_density=effective_dashboards_table_density,\n email_address=effective_email_address,\n )\n if validation_errors:\n logger.reflect(\"Validation failed; mutation is denied\")\n raise ProfileValidationError(validation_errors)\n\n preference.superset_username = effective_superset_username\n preference.superset_username_normalized = normalize_username(\n effective_superset_username\n )\n preference.show_only_my_dashboards = effective_show_only\n preference.show_only_slug_dashboards = effective_show_only_slug\n\n preference.git_username = effective_git_username\n preference.git_email = effective_git_email\n\n if \"git_personal_access_token\" in provided_fields:\n sanitized_token = sanitize_secret(\n payload.git_personal_access_token\n )\n if sanitized_token is None:\n preference.git_personal_access_token_encrypted = None\n else:\n preference.git_personal_access_token_encrypted = (\n self.encryption.encrypt(sanitized_token)\n )\n\n preference.start_page = effective_start_page\n preference.auto_open_task_drawer = effective_auto_open_task_drawer\n preference.dashboards_table_density = effective_dashboards_table_density\n preference.telegram_id = effective_telegram_id\n preference.email_address = effective_email_address\n preference.notify_on_fail = effective_notify_on_fail\n preference.updated_at = datetime.now(UTC)\n\n persisted_preference = self.auth_repository.save_user_dashboard_preference(\n preference\n )\n\n logger.reason(\"Preference persisted successfully\")\n return ProfilePreferenceResponse(\n status=\"success\",\n message=\"Preference saved\",\n preference=self._to_preference_payload(\n persisted_preference,\n str(current_user.id),\n ),\n security=self.security_badge_service.build_security_summary(current_user),\n )\n # #endregion update_my_preference\n" }, { "contract_id": "_to_preference_payload", @@ -47749,7 +48629,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.utcnow()\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n" + "body": " # #region _to_preference_payload [TYPE Function]\n # @BRIEF Map ORM preference row to API DTO with token metadata.\n # @PRE preference row can contain nullable optional fields.\n # @POST Returns normalized ProfilePreference object.\n def _to_preference_payload(\n self,\n preference: UserDashboardPreference,\n user_id: str,\n ) -> ProfilePreference:\n encrypted_token = sanitize_text(\n preference.git_personal_access_token_encrypted\n )\n token_masked = None\n if encrypted_token:\n try:\n decrypted_token = self.encryption.decrypt(encrypted_token)\n token_masked = mask_secret_value(decrypted_token)\n except Exception:\n token_masked = \"***\"\n\n created_at = getattr(preference, \"created_at\", None) or datetime.now(UTC)\n updated_at = getattr(preference, \"updated_at\", None) or created_at\n\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=sanitize_username(preference.superset_username),\n superset_username_normalized=normalize_username(\n preference.superset_username_normalized\n ),\n show_only_my_dashboards=bool(preference.show_only_my_dashboards),\n show_only_slug_dashboards=(\n bool(preference.show_only_slug_dashboards)\n if preference.show_only_slug_dashboards is not None\n else True\n ),\n git_username=sanitize_text(preference.git_username),\n git_email=sanitize_text(preference.git_email),\n has_git_personal_access_token=bool(encrypted_token),\n git_personal_access_token_masked=token_masked,\n start_page=normalize_start_page(preference.start_page),\n auto_open_task_drawer=(\n bool(preference.auto_open_task_drawer)\n if preference.auto_open_task_drawer is not None\n else True\n ),\n dashboards_table_density=normalize_density(\n preference.dashboards_table_density\n ),\n telegram_id=sanitize_text(preference.telegram_id),\n email_address=sanitize_text(preference.email_address),\n notify_on_fail=bool(preference.notify_on_fail)\n if preference.notify_on_fail is not None\n else True,\n created_at=created_at,\n updated_at=updated_at,\n )\n # #endregion _to_preference_payload\n" }, { "contract_id": "_build_default_preference", @@ -47810,7 +48690,7 @@ "contract_type": "Module", "file_path": "backend/src/services/profile_service.py", "start_line": 1, - "end_line": 172, + "end_line": 170, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -47821,14 +48701,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region profile_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, preference, superset, lookup]\n#\n# @BRIEF Composite facade orchestrating profile preference persistence, Superset account lookup,\n# security badges, and deterministic actor matching by delegating to focused sub-services.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION DEPENDS_ON -> [ProfilePreferenceResponse]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AuthRepositoryModule]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [ProfilePreferenceService]\n# @RELATION DEPENDS_ON -> [SupersetLookupService]\n# @RELATION DEPENDS_ON -> [SecurityBadgeService]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n#\n# @INVARIANT Profile ID needs to be unique per-user session\n#\n# @TEST_CONTRACT ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse\n# @TEST_FIXTURE valid_profile_update -> {\"user_id\":\"u-1\",\"superset_username\":\"John_Doe\",\"show_only_my_dashboards\":true}\n# @TEST_EDGE enable_without_username -> toggle=true with empty username returns validation error\n# @TEST_EDGE cross_user_mutation -> attempt to update another user preference returns forbidden\n# @TEST_EDGE lookup_env_not_found -> unknown environment_id returns not found\n# @TEST_INVARIANT normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]\n# @DATA_CONTRACT Profile_id -> ProfileInfo; session_id -> valid UUID\n# @PRE Session is active and valid\n# @POST Profile with updated fields populated and\n# @SIDE_EFFECT Database read/write operations\n# @RATIONALE Decomposed from monolithic 770-line ProfileService into three focused services\n# (ProfilePreferenceService, SupersetLookupService, SecurityBadgeService) plus pure\n# utility functions (ProfileUtils) to satisfy INV_7. This module is a thin facade\n# that preserves the public API contract for all existing callers.\n# @REJECTED Keeping all three domains in a single ProfileService class was rejected — it violated\n# INV_7 (770 lines vs 150 max), mixed I/O patterns (DB writes + HTTP calls), and created\n# unnecessary coupling between preference persistence and Superset network operations.\n\nfrom collections.abc import Iterable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import belief_scope, logger\nfrom ..models.auth import User\nfrom ..schemas.profile import (\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom .profile_preference_service import ProfilePreferenceService\nfrom .profile_utils import (\n EnvironmentNotFoundError,\n ProfileAuthorizationError,\n ProfileValidationError,\n normalize_owner_tokens,\n normalize_username,\n sanitize_username,\n)\nfrom .security_badge_service import SecurityBadgeService\nfrom .superset_lookup_service import SupersetLookupService\n\n# Re-export exception classes for backward-compatible imports\n__all__ = [\n \"EnvironmentNotFoundError\",\n \"ProfileAuthorizationError\",\n \"ProfilePreferenceService\",\n \"ProfileService\",\n \"ProfileValidationError\",\n \"SecurityBadgeService\",\n \"SupersetLookupService\",\n]\n\n\n# #region ProfileService [C:4] [TYPE Class] [SEMANTICS profile,facade,delegate,coordinator]\n# @BRIEF Facade that composes ProfilePreferenceService, SupersetLookupService, and\n# SecurityBadgeService into a single interface for backward compatibility.\n# @RELATION DEPENDS_ON -> [ProfilePreferenceService]\n# @RELATION DEPENDS_ON -> [SupersetLookupService]\n# @RELATION DEPENDS_ON -> [SecurityBadgeService]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @PRE Caller provides authenticated User context for external service methods.\n# @POST Delegates to sub-services and returns normalized profile/lookup responses.\n# @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested.\n# @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]\n# @INVARIANT Profile data integrity maintained, cache consistency with database state\n# @RATIONALE Thin coordinator — all sub-10-line delegating methods. Actual business logic lives\n# in the injected sub-services.\nclass ProfileService:\n \"\"\"Facade composing preference, lookup, and security services.\"\"\"\n\n # region init [TYPE Function]\n # @BRIEF Initialize facade and create sub-services.\n # @PRE db session is active and config_manager supports get_environments().\n # @POST All sub-services are initialized and ready.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.preference_service = ProfilePreferenceService(db, config_manager, plugin_loader)\n self.lookup_service = SupersetLookupService(config_manager)\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n\n # endregion init\n\n # region get_my_preference [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n return self.preference_service.get_my_preference(current_user)\n\n # endregion get_my_preference\n\n # region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n return self.preference_service.get_dashboard_filter_binding(current_user)\n\n # endregion get_dashboard_filter_binding\n\n # region update_my_preference [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n return self.preference_service.update_my_preference(\n current_user, payload, target_user_id\n )\n\n # endregion update_my_preference\n\n # region lookup_superset_accounts [TYPE Function]\n # @BRIEF Delegate to SupersetLookupService.\n def lookup_superset_accounts(\n self,\n current_user: User,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n return self.lookup_service.lookup_superset_accounts(current_user, request)\n\n # endregion lookup_superset_accounts\n\n # region matches_dashboard_actor [TYPE Function]\n # @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by.\n # @PRE bound_username can be empty; owners may contain mixed payload.\n # @POST Returns True when normalized username matches owners or modified_by.\n def matches_dashboard_actor(\n self,\n bound_username: str | None,\n owners: Iterable[Any] | None,\n modified_by: str | None,\n ) -> bool:\n normalized_actor = normalize_username(bound_username)\n if not normalized_actor:\n return False\n\n owner_tokens = normalize_owner_tokens(owners)\n modified_token = normalize_username(modified_by)\n\n if normalized_actor in owner_tokens:\n return True\n if modified_token and normalized_actor == modified_token:\n return True\n return False\n\n # endregion matches_dashboard_actor\n\n\n# #endregion ProfileService\n\n# #endregion profile_service\n" + "body": "# #region profile_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, profile, preference, superset, lookup]\n#\n# @BRIEF Composite facade orchestrating profile preference persistence, Superset account lookup,\n# security badges, and deterministic actor matching by delegating to focused sub-services.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [UserDashboardPreference]\n# @RELATION DEPENDS_ON -> [ProfilePreferenceResponse]\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [AuthRepositoryModule]\n# @RELATION DEPENDS_ON -> [User]\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.orm.Session]\n# @RELATION DEPENDS_ON -> [ProfilePreferenceService]\n# @RELATION DEPENDS_ON -> [SupersetLookupService]\n# @RELATION DEPENDS_ON -> [SecurityBadgeService]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n#\n# @INVARIANT Profile ID needs to be unique per-user session\n#\n# @TEST_CONTRACT ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse\n# @TEST_FIXTURE valid_profile_update -> {\"user_id\":\"u-1\",\"superset_username\":\"John_Doe\",\"show_only_my_dashboards\":true}\n# @TEST_EDGE enable_without_username -> toggle=true with empty username returns validation error\n# @TEST_EDGE cross_user_mutation -> attempt to update another user preference returns forbidden\n# @TEST_EDGE lookup_env_not_found -> unknown environment_id returns not found\n# @TEST_INVARIANT normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]\n# @DATA_CONTRACT Profile_id -> ProfileInfo; session_id -> valid UUID\n# @PRE Session is active and valid\n# @POST Profile with updated fields populated and\n# @SIDE_EFFECT Database read/write operations\n# @RATIONALE Decomposed from monolithic 770-line ProfileService into three focused services\n# (ProfilePreferenceService, SupersetLookupService, SecurityBadgeService) plus pure\n# utility functions (ProfileUtils) to satisfy INV_7. This module is a thin facade\n# that preserves the public API contract for all existing callers.\n# @REJECTED Keeping all three domains in a single ProfileService class was rejected — it violated\n# INV_7 (770 lines vs 150 max), mixed I/O patterns (DB writes + HTTP calls), and created\n# unnecessary coupling between preference persistence and Superset network operations.\n\nfrom collections.abc import Iterable\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..models.auth import User\nfrom ..schemas.profile import (\n ProfilePreferenceResponse,\n ProfilePreferenceUpdateRequest,\n SupersetAccountLookupRequest,\n SupersetAccountLookupResponse,\n)\nfrom .profile_preference_service import ProfilePreferenceService\nfrom .profile_utils import (\n EnvironmentNotFoundError,\n ProfileAuthorizationError,\n ProfileValidationError,\n normalize_owner_tokens,\n normalize_username,\n)\nfrom .security_badge_service import SecurityBadgeService\nfrom .superset_lookup_service import SupersetLookupService\n\n# Re-export exception classes for backward-compatible imports\n__all__ = [\n \"EnvironmentNotFoundError\",\n \"ProfileAuthorizationError\",\n \"ProfilePreferenceService\",\n \"ProfileService\",\n \"ProfileValidationError\",\n \"SecurityBadgeService\",\n \"SupersetLookupService\",\n]\n\n\n# #region ProfileService [C:4] [TYPE Class] [SEMANTICS profile,facade,delegate,coordinator]\n# @BRIEF Facade that composes ProfilePreferenceService, SupersetLookupService, and\n# SecurityBadgeService into a single interface for backward compatibility.\n# @RELATION DEPENDS_ON -> [ProfilePreferenceService]\n# @RELATION DEPENDS_ON -> [SupersetLookupService]\n# @RELATION DEPENDS_ON -> [SecurityBadgeService]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @PRE Caller provides authenticated User context for external service methods.\n# @POST Delegates to sub-services and returns normalized profile/lookup responses.\n# @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested.\n# @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]\n# @INVARIANT Profile data integrity maintained, cache consistency with database state\n# @RATIONALE Thin coordinator — all sub-10-line delegating methods. Actual business logic lives\n# in the injected sub-services.\nclass ProfileService:\n \"\"\"Facade composing preference, lookup, and security services.\"\"\"\n\n # region init [TYPE Function]\n # @BRIEF Initialize facade and create sub-services.\n # @PRE db session is active and config_manager supports get_environments().\n # @POST All sub-services are initialized and ready.\n def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):\n self.preference_service = ProfilePreferenceService(db, config_manager, plugin_loader)\n self.lookup_service = SupersetLookupService(config_manager)\n self.security_badge_service = SecurityBadgeService(plugin_loader)\n self.db = db\n self.config_manager = config_manager\n self.plugin_loader = plugin_loader\n\n # endregion init\n\n # region get_my_preference [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def get_my_preference(self, current_user: User) -> ProfilePreferenceResponse:\n return self.preference_service.get_my_preference(current_user)\n\n # endregion get_my_preference\n\n # region get_dashboard_filter_binding [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def get_dashboard_filter_binding(self, current_user: User) -> dict:\n return self.preference_service.get_dashboard_filter_binding(current_user)\n\n # endregion get_dashboard_filter_binding\n\n # region update_my_preference [TYPE Function]\n # @BRIEF Delegate to ProfilePreferenceService.\n def update_my_preference(\n self,\n current_user: User,\n payload: ProfilePreferenceUpdateRequest,\n target_user_id: str | None = None,\n ) -> ProfilePreferenceResponse:\n return self.preference_service.update_my_preference(\n current_user, payload, target_user_id\n )\n\n # endregion update_my_preference\n\n # region lookup_superset_accounts [TYPE Function]\n # @BRIEF Delegate to SupersetLookupService.\n def lookup_superset_accounts(\n self,\n current_user: User,\n request: SupersetAccountLookupRequest,\n ) -> SupersetAccountLookupResponse:\n return self.lookup_service.lookup_superset_accounts(current_user, request)\n\n # endregion lookup_superset_accounts\n\n # region matches_dashboard_actor [TYPE Function]\n # @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by.\n # @PRE bound_username can be empty; owners may contain mixed payload.\n # @POST Returns True when normalized username matches owners or modified_by.\n def matches_dashboard_actor(\n self,\n bound_username: str | None,\n owners: Iterable[Any] | None,\n modified_by: str | None,\n ) -> bool:\n normalized_actor = normalize_username(bound_username)\n if not normalized_actor:\n return False\n\n owner_tokens = normalize_owner_tokens(owners)\n modified_token = normalize_username(modified_by)\n\n if normalized_actor in owner_tokens:\n return True\n if modified_token and normalized_actor == modified_token:\n return True\n return False\n\n # endregion matches_dashboard_actor\n\n\n# #endregion ProfileService\n\n# #endregion profile_service\n" }, { "contract_id": "ProfileService", "contract_type": "Class", "file_path": "backend/src/services/profile_service.py", - "start_line": 74, - "end_line": 170, + "start_line": 72, + "end_line": 168, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -47857,7 +48737,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ProfileUtils [C:2] [TYPE Module] [SEMANTICS string,normalization,utility,sanitize]\n# @BRIEF Pure utility helpers for profile data sanitization, normalization, and secret masking.\n# Also contains shared exception classes to avoid circular imports between profile sub-modules.\n# @LAYER Domain\n# @RATIONALE Extracted from ProfileService to satisfy INV_7 — module length under 400 lines,\n# contract length under 150 lines. These are stateless pure functions that do not\n# require a class or service container.\n# @REJECTED Keeping these as private methods on ProfileService was rejected because they\n# inflate the class beyond 150 lines and mix pure computation with I/O-bound\n# service logic.\n\nfrom collections.abc import Iterable, Sequence\nfrom datetime import datetime\nfrom typing import Any\n\n\n# #region ProfileValidationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Domain validation error for profile preference update requests.\nclass ProfileValidationError(Exception):\n def __init__(self, errors: Sequence[str]):\n self.errors = list(errors)\n super().__init__(\"Profile preference validation failed\")\n# #endregion ProfileValidationError\n\n\n# #region EnvironmentNotFoundError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n# #endregion EnvironmentNotFoundError\n\n\n# #region ProfileAuthorizationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n# #endregion ProfileAuthorizationError\n\nSUPPORTED_START_PAGES: set[str] = {\"dashboards\", \"datasets\", \"reports\"}\nSUPPORTED_DENSITIES: set[str] = {\"compact\", \"comfortable\"}\n\n\n# #region sanitize_text [C:1] [TYPE Function]\n# @BRIEF Normalize optional text into trimmed form or None.\ndef sanitize_text(value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# #endregion sanitize_text\n\n\n# #region sanitize_secret [C:1] [TYPE Function]\n# @BRIEF Normalize secret input into trimmed form or None.\ndef sanitize_secret(value: str | None) -> str | None:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n# #endregion sanitize_secret\n\n\n# #region sanitize_username [C:1] [TYPE Function]\n# @BRIEF Normalize raw username into trimmed form or None for empty input.\ndef sanitize_username(value: str | None) -> str | None:\n return sanitize_text(value)\n# #endregion sanitize_username\n\n\n# #region normalize_username [C:1] [TYPE Function]\n# @BRIEF Apply deterministic trim+lower normalization for actor matching.\ndef normalize_username(value: str | None) -> str | None:\n sanitized = sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n# #endregion normalize_username\n\n\n# #region normalize_start_page [C:1] [TYPE Function]\n# @BRIEF Normalize supported start page aliases to canonical values.\ndef normalize_start_page(value: str | None) -> str:\n normalized = str(value or \"\").strip().lower()\n if normalized == \"reports-logs\":\n return \"reports\"\n if normalized in SUPPORTED_START_PAGES:\n return normalized\n return \"dashboards\"\n# #endregion normalize_start_page\n\n\n# #region normalize_density [C:1] [TYPE Function]\n# @BRIEF Normalize supported density aliases to canonical values.\ndef normalize_density(value: str | None) -> str:\n normalized = str(value or \"\").strip().lower()\n if normalized == \"free\":\n return \"comfortable\"\n if normalized in SUPPORTED_DENSITIES:\n return normalized\n return \"comfortable\"\n# #endregion normalize_density\n\n\n# #region mask_secret_value [C:1] [TYPE Function]\n# @BRIEF Build a safe display value for sensitive secrets.\ndef mask_secret_value(secret: str | None) -> str | None:\n sanitized = sanitize_secret(secret)\n if sanitized is None:\n return None\n if len(sanitized) <= 4:\n return \"***\"\n return f\"{sanitized[:2]}***{sanitized[-2:]}\"\n# #endregion mask_secret_value\n\n\n# #region validate_update_payload [C:2] [TYPE Function]\n# @BRIEF Validate username/toggle constraints for preference mutation.\n# @RELATION CALLS -> [sanitize_username]\n# @RELATION CALLS -> [sanitize_text]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @POST Returns validation errors list; empty list means valid.\ndef validate_update_payload(\n superset_username: str | None,\n show_only_my_dashboards: bool,\n git_email: str | None,\n start_page: str,\n dashboards_table_density: str,\n email_address: str | None = None,\n) -> list[str]:\n errors: list[str] = []\n sanitized_username = sanitize_username(superset_username)\n\n if sanitized_username and any(ch.isspace() for ch in sanitized_username):\n errors.append(\n \"Username should not contain spaces. Please enter a valid Apache Superset username.\"\n )\n if show_only_my_dashboards and not sanitized_username:\n errors.append(\n \"Superset username is required when default filter is enabled.\"\n )\n\n sanitized_git_email = sanitize_text(git_email)\n if sanitized_git_email:\n if (\n \" \" in sanitized_git_email\n or \"@\" not in sanitized_git_email\n or sanitized_git_email.startswith(\"@\")\n or sanitized_git_email.endswith(\"@\")\n ):\n errors.append(\"Git email should be a valid email address.\")\n\n if start_page not in SUPPORTED_START_PAGES:\n errors.append(\"Start page value is not supported.\")\n\n if dashboards_table_density not in SUPPORTED_DENSITIES:\n errors.append(\"Dashboards table density value is not supported.\")\n\n sanitized_email = sanitize_text(email_address)\n if sanitized_email:\n if (\n \" \" in sanitized_email\n or \"@\" not in sanitized_email\n or sanitized_email.startswith(\"@\")\n or sanitized_email.endswith(\"@\")\n ):\n errors.append(\"Notification email should be a valid email address.\")\n\n return errors\n# #endregion validate_update_payload\n\n\n# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]\n# @BRIEF Build non-persisted default preference DTO for unconfigured users.\n# @RELATION DEPENDS_ON -> [ProfilePreference]\ndef build_default_preference(user_id: str) -> Any:\n \"\"\"Build default ProfilePreference with disabled toggles and empty usernames.\"\"\"\n from ..schemas.profile import ProfilePreference\n now = datetime.utcnow()\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=None,\n superset_username_normalized=None,\n show_only_my_dashboards=False,\n show_only_slug_dashboards=True,\n git_username=None,\n git_email=None,\n has_git_personal_access_token=False,\n git_personal_access_token_masked=None,\n start_page=\"dashboards\",\n auto_open_task_drawer=True,\n dashboards_table_density=\"comfortable\",\n telegram_id=None,\n email_address=None,\n notify_on_fail=True,\n created_at=now,\n updated_at=now,\n )\n# #endregion build_default_preference\n\n\n# #region normalize_owner_tokens [C:2] [TYPE Function]\n# @BRIEF Normalize owners payload into deduplicated lower-cased tokens.\n# @RELATION CALLS -> [normalize_username]\ndef normalize_owner_tokens(owners: Iterable[Any] | None) -> list[str]:\n if owners is None:\n return []\n normalized: list[str] = []\n for owner in owners:\n owner_candidates: list[Any]\n if isinstance(owner, dict):\n first_name = sanitize_username(str(owner.get(\"first_name\") or \"\"))\n last_name = sanitize_username(str(owner.get(\"last_name\") or \"\"))\n full_name = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n snake_name = \"_\".join(\n part for part in [first_name, last_name] if part\n ).strip(\"_\")\n owner_candidates = [\n owner.get(\"username\"),\n owner.get(\"user_name\"),\n owner.get(\"name\"),\n owner.get(\"full_name\"),\n first_name,\n last_name,\n full_name or None,\n snake_name or None,\n owner.get(\"email\"),\n ]\n else:\n owner_candidates = [owner]\n\n for candidate in owner_candidates:\n token = normalize_username(str(candidate or \"\"))\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n# #endregion normalize_owner_tokens\n# #endregion ProfileUtils\n" + "body": "# #region ProfileUtils [C:2] [TYPE Module] [SEMANTICS string,normalization,utility,sanitize]\n# @BRIEF Pure utility helpers for profile data sanitization, normalization, and secret masking.\n# Also contains shared exception classes to avoid circular imports between profile sub-modules.\n# @LAYER Domain\n# @RATIONALE Extracted from ProfileService to satisfy INV_7 — module length under 400 lines,\n# contract length under 150 lines. These are stateless pure functions that do not\n# require a class or service container.\n# @REJECTED Keeping these as private methods on ProfileService was rejected because they\n# inflate the class beyond 150 lines and mix pure computation with I/O-bound\n# service logic.\n\nfrom collections.abc import Iterable, Sequence\nfrom datetime import UTC, datetime\nfrom typing import Any\n\n\n# #region ProfileValidationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Domain validation error for profile preference update requests.\nclass ProfileValidationError(Exception):\n def __init__(self, errors: Sequence[str]):\n self.errors = list(errors)\n super().__init__(\"Profile preference validation failed\")\n# #endregion ProfileValidationError\n\n\n# #region EnvironmentNotFoundError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.\nclass EnvironmentNotFoundError(Exception):\n pass\n# #endregion EnvironmentNotFoundError\n\n\n# #region ProfileAuthorizationError [C:2] [TYPE Class]\n# @RELATION INHERITS -> [EXT:Python:Exception]\n# @BRIEF Raised when caller attempts cross-user preference mutation.\nclass ProfileAuthorizationError(Exception):\n pass\n# #endregion ProfileAuthorizationError\n\nSUPPORTED_START_PAGES: set[str] = {\"dashboards\", \"datasets\", \"reports\"}\nSUPPORTED_DENSITIES: set[str] = {\"compact\", \"comfortable\"}\n\n\n# #region sanitize_text [C:1] [TYPE Function]\n# @BRIEF Normalize optional text into trimmed form or None.\ndef sanitize_text(value: str | None) -> str | None:\n normalized = str(value or \"\").strip()\n if not normalized:\n return None\n return normalized\n# #endregion sanitize_text\n\n\n# #region sanitize_secret [C:1] [TYPE Function]\n# @BRIEF Normalize secret input into trimmed form or None.\ndef sanitize_secret(value: str | None) -> str | None:\n if value is None:\n return None\n normalized = str(value).strip()\n if not normalized:\n return None\n return normalized\n# #endregion sanitize_secret\n\n\n# #region sanitize_username [C:1] [TYPE Function]\n# @BRIEF Normalize raw username into trimmed form or None for empty input.\ndef sanitize_username(value: str | None) -> str | None:\n return sanitize_text(value)\n# #endregion sanitize_username\n\n\n# #region normalize_username [C:1] [TYPE Function]\n# @BRIEF Apply deterministic trim+lower normalization for actor matching.\ndef normalize_username(value: str | None) -> str | None:\n sanitized = sanitize_username(value)\n if sanitized is None:\n return None\n return sanitized.lower()\n# #endregion normalize_username\n\n\n# #region normalize_start_page [C:1] [TYPE Function]\n# @BRIEF Normalize supported start page aliases to canonical values.\ndef normalize_start_page(value: str | None) -> str:\n normalized = str(value or \"\").strip().lower()\n if normalized == \"reports-logs\":\n return \"reports\"\n if normalized in SUPPORTED_START_PAGES:\n return normalized\n return \"dashboards\"\n# #endregion normalize_start_page\n\n\n# #region normalize_density [C:1] [TYPE Function]\n# @BRIEF Normalize supported density aliases to canonical values.\ndef normalize_density(value: str | None) -> str:\n normalized = str(value or \"\").strip().lower()\n if normalized == \"free\":\n return \"comfortable\"\n if normalized in SUPPORTED_DENSITIES:\n return normalized\n return \"comfortable\"\n# #endregion normalize_density\n\n\n# #region mask_secret_value [C:1] [TYPE Function]\n# @BRIEF Build a safe display value for sensitive secrets.\ndef mask_secret_value(secret: str | None) -> str | None:\n sanitized = sanitize_secret(secret)\n if sanitized is None:\n return None\n if len(sanitized) <= 4:\n return \"***\"\n return f\"{sanitized[:2]}***{sanitized[-2:]}\"\n# #endregion mask_secret_value\n\n\n# #region validate_update_payload [C:2] [TYPE Function]\n# @BRIEF Validate username/toggle constraints for preference mutation.\n# @RELATION CALLS -> [sanitize_username]\n# @RELATION CALLS -> [sanitize_text]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @RELATION DEPENDS_ON -> [ProfileUtils]\n# @POST Returns validation errors list; empty list means valid.\ndef validate_update_payload(\n superset_username: str | None,\n show_only_my_dashboards: bool,\n git_email: str | None,\n start_page: str,\n dashboards_table_density: str,\n email_address: str | None = None,\n) -> list[str]:\n errors: list[str] = []\n sanitized_username = sanitize_username(superset_username)\n\n if sanitized_username and any(ch.isspace() for ch in sanitized_username):\n errors.append(\n \"Username should not contain spaces. Please enter a valid Apache Superset username.\"\n )\n if show_only_my_dashboards and not sanitized_username:\n errors.append(\n \"Superset username is required when default filter is enabled.\"\n )\n\n sanitized_git_email = sanitize_text(git_email)\n if sanitized_git_email:\n if (\n \" \" in sanitized_git_email\n or \"@\" not in sanitized_git_email\n or sanitized_git_email.startswith(\"@\")\n or sanitized_git_email.endswith(\"@\")\n ):\n errors.append(\"Git email should be a valid email address.\")\n\n if start_page not in SUPPORTED_START_PAGES:\n errors.append(\"Start page value is not supported.\")\n\n if dashboards_table_density not in SUPPORTED_DENSITIES:\n errors.append(\"Dashboards table density value is not supported.\")\n\n sanitized_email = sanitize_text(email_address)\n if sanitized_email:\n if (\n \" \" in sanitized_email\n or \"@\" not in sanitized_email\n or sanitized_email.startswith(\"@\")\n or sanitized_email.endswith(\"@\")\n ):\n errors.append(\"Notification email should be a valid email address.\")\n\n return errors\n# #endregion validate_update_payload\n\n\n# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]\n# @BRIEF Build non-persisted default preference DTO for unconfigured users.\n# @RELATION DEPENDS_ON -> [ProfilePreference]\ndef build_default_preference(user_id: str) -> Any:\n \"\"\"Build default ProfilePreference with disabled toggles and empty usernames.\"\"\"\n from ..schemas.profile import ProfilePreference\n now = datetime.now(UTC)\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=None,\n superset_username_normalized=None,\n show_only_my_dashboards=False,\n show_only_slug_dashboards=True,\n git_username=None,\n git_email=None,\n has_git_personal_access_token=False,\n git_personal_access_token_masked=None,\n start_page=\"dashboards\",\n auto_open_task_drawer=True,\n dashboards_table_density=\"comfortable\",\n telegram_id=None,\n email_address=None,\n notify_on_fail=True,\n created_at=now,\n updated_at=now,\n )\n# #endregion build_default_preference\n\n\n# #region normalize_owner_tokens [C:2] [TYPE Function]\n# @BRIEF Normalize owners payload into deduplicated lower-cased tokens.\n# @RELATION CALLS -> [normalize_username]\ndef normalize_owner_tokens(owners: Iterable[Any] | None) -> list[str]:\n if owners is None:\n return []\n normalized: list[str] = []\n for owner in owners:\n owner_candidates: list[Any]\n if isinstance(owner, dict):\n first_name = sanitize_username(str(owner.get(\"first_name\") or \"\"))\n last_name = sanitize_username(str(owner.get(\"last_name\") or \"\"))\n full_name = \" \".join(\n part for part in [first_name, last_name] if part\n ).strip()\n snake_name = \"_\".join(\n part for part in [first_name, last_name] if part\n ).strip(\"_\")\n owner_candidates = [\n owner.get(\"username\"),\n owner.get(\"user_name\"),\n owner.get(\"name\"),\n owner.get(\"full_name\"),\n first_name,\n last_name,\n full_name or None,\n snake_name or None,\n owner.get(\"email\"),\n ]\n else:\n owner_candidates = [owner]\n\n for candidate in owner_candidates:\n token = normalize_username(str(candidate or \"\"))\n if token and token not in normalized:\n normalized.append(token)\n return normalized\n# #endregion normalize_owner_tokens\n# #endregion ProfileUtils\n" }, { "contract_id": "ProfileValidationError", @@ -48127,7 +49007,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]\n# @BRIEF Build non-persisted default preference DTO for unconfigured users.\n# @RELATION DEPENDS_ON -> [ProfilePreference]\ndef build_default_preference(user_id: str) -> Any:\n \"\"\"Build default ProfilePreference with disabled toggles and empty usernames.\"\"\"\n from ..schemas.profile import ProfilePreference\n now = datetime.utcnow()\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=None,\n superset_username_normalized=None,\n show_only_my_dashboards=False,\n show_only_slug_dashboards=True,\n git_username=None,\n git_email=None,\n has_git_personal_access_token=False,\n git_personal_access_token_masked=None,\n start_page=\"dashboards\",\n auto_open_task_drawer=True,\n dashboards_table_density=\"comfortable\",\n telegram_id=None,\n email_address=None,\n notify_on_fail=True,\n created_at=now,\n updated_at=now,\n )\n# #endregion build_default_preference\n" + "body": "# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]\n# @BRIEF Build non-persisted default preference DTO for unconfigured users.\n# @RELATION DEPENDS_ON -> [ProfilePreference]\ndef build_default_preference(user_id: str) -> Any:\n \"\"\"Build default ProfilePreference with disabled toggles and empty usernames.\"\"\"\n from ..schemas.profile import ProfilePreference\n now = datetime.now(UTC)\n return ProfilePreference(\n user_id=str(user_id),\n superset_username=None,\n superset_username_normalized=None,\n show_only_my_dashboards=False,\n show_only_slug_dashboards=True,\n git_username=None,\n git_email=None,\n has_git_personal_access_token=False,\n git_personal_access_token_masked=None,\n start_page=\"dashboards\",\n auto_open_task_drawer=True,\n dashboards_table_density=\"comfortable\",\n telegram_id=None,\n email_address=None,\n notify_on_fail=True,\n created_at=now,\n updated_at=now,\n )\n# #endregion build_default_preference\n" }, { "contract_id": "normalize_owner_tokens", @@ -48405,7 +49285,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region normalizer [C:5] [TYPE Module] [SEMANTICS pydantic, report, task, normalize, status]\n# @BRIEF Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:frontend:TaskModel]\n# @RELATION DEPENDS_ON -> [EXT:frontend:ReportModel]\n# @RELATION DEPENDS_ON -> [EXT:frontend:TypeProfiles]\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\nfrom datetime import datetime\nfrom typing import Any\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\n\n# #region status_to_report_status [TYPE Function]\n# @BRIEF 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.\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# #endregion status_to_report_status\n\n\n# #region build_summary [TYPE Function]\n# @BRIEF 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.\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# #endregion build_summary\n\n\n# #region extract_error_context [TYPE Function]\n# @BRIEF 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.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> ErrorContext | None:\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# #endregion extract_error_context\n\n\n# #region normalize_task_report [TYPE Function]\n# @BRIEF 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#\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# #endregion normalize_task_report\n\n# #endregion normalizer\n" + "body": "# #region normalizer [C:5] [TYPE Module] [SEMANTICS pydantic, report, task, normalize, status]\n# @BRIEF Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [EXT:frontend:TaskModel]\n# @RELATION DEPENDS_ON -> [EXT:frontend:ReportModel]\n# @RELATION DEPENDS_ON -> [EXT:frontend:TypeProfiles]\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\nfrom datetime import UTC, datetime\nfrom typing import Any\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\n\n# #region status_to_report_status [TYPE Function]\n# @BRIEF 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.\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# #endregion status_to_report_status\n\n\n# #region build_summary [TYPE Function]\n# @BRIEF 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.\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# #endregion build_summary\n\n\n# #region extract_error_context [TYPE Function]\n# @BRIEF 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.\ndef extract_error_context(task: Task, report_status: ReportStatus) -> ErrorContext | None:\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# #endregion extract_error_context\n\n\n# #region normalize_task_report [TYPE Function]\n# @BRIEF 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#\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.now(UTC)\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# #endregion normalize_task_report\n\n# #endregion normalizer\n" }, { "contract_id": "status_to_report_status", @@ -48482,7 +49362,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region normalize_task_report [TYPE Function]\n# @BRIEF 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#\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# #endregion normalize_task_report\n" + "body": "# #region normalize_task_report [TYPE Function]\n# @BRIEF 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#\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.now(UTC)\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# #endregion normalize_task_report\n" }, { "contract_id": "report_service", @@ -48731,7 +49611,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset]\n# @BRIEF Shared service for fetching resource data with Git status and task status\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [TaskManagerModels]\n# @RELATION DEPENDS_ON -> [GitService]\n# @INVARIANT All resources include metadata about their current state\n# @SIDE_EFFECT Queries multiple backends for status\n# @DATA_CONTRACT ResourceQuery -> ResourceStatusSummary\n\nimport asyncio\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.models import Task\nfrom ..services.git_service import GitService\n\n\n# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE None\n # @POST ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of dashboards with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE env is valid; page >= 1; page_size > 0.\n # @POST Returns page items plus total counters without scanning all pages locally.\n # @PARAM env (Environment) - Source environment.\n # @PARAM tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM page (int) - 1-based page number.\n # @PARAM page_size (int) - Page size.\n # @RETURN Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE dashboard_id is a valid integer identifier\n # @POST Returns the newest llm_dashboard_validation task summary or None\n # @PARAM dashboard_id (int) - The dashboard ID\n # @PARAM env_id (Optional[str]) - Environment ID to match task params\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n # @RELATION CALLS ->[EXT:method:_normalize_validation_status]\n # @RELATION CALLS ->[EXT:method:_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE raw_status can be enum or string\n # @POST Returns uppercase status without enum class prefix\n # @PARAM raw_status (Any) - Raw task status object/value\n # @RETURN str - Normalized status token\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE raw_status can be any scalar type\n # @POST Returns normalized validation status token or None\n # @PARAM raw_status (Any) - Raw validation status from task result\n # @RETURN Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE value may be datetime or any scalar.\n # @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM value (Any) - Candidate datetime-like value.\n # @RETURN datetime - UTC-aware comparable datetime.\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n # @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of datasets with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields\n # @RELATION CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]\n # @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]\n # @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database\n # StatsBar showed linked_count=0 because the field was never populated in list endpoint.\n # Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).\n # @REJECTED N+1 blocking calls rejected — would timeout at 26×1s. Semaphore + asyncio.to_thread + timeout prevents overload.\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status and linked dashboard count\n sem = asyncio.Semaphore(3) # limit concurrent Superset calls\n\n async def fetch_linked_count(ds_id: int) -> int:\n async with sem:\n try:\n return await asyncio.wait_for(\n asyncio.to_thread(\n client.get_dataset_linked_dashboard_count, ds_id\n ),\n timeout=10.0,\n )\n except (asyncio.TimeoutError, Exception):\n logger.warning(\n f\"[EXT:method:get_datasets_with_status][Warning] \"\n f\"Failed to fetch linked dashboard count for dataset {ds_id}\"\n )\n return 0\n\n result = []\n linked_tasks = []\n for dataset in datasets:\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n linked_tasks.append(fetch_linked_count(dataset_id))\n\n # Fetch linked dashboard counts concurrently — FR-022\n if linked_tasks:\n linked_counts = await asyncio.gather(*linked_tasks)\n for ds, count in zip(result, linked_counts):\n ds['linked_dashboard_count'] = count\n\n logger.info(\n f\"[ResourceService][Coherence:OK] \"\n f\"Fetched {len(result)} datasets with status and linked counts\"\n )\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE tasks is a list of Task objects\n # @POST Returns summary with active_count and recent_tasks\n # @PARAM tasks (List[Task]) - List of tasks to summarize\n # @RETURN Dict - Activity summary\n # @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]\n # @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE dashboard_id is a valid integer\n # @POST Returns git status or None if no repo exists\n # @PARAM dashboard_id (int) - The dashboard ID\n # @RETURN Optional[Dict] - Git status with branch and sync_status\n # @RELATION CALLS ->[EXT:method:get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE resource_id is a valid string\n # @POST Returns task summary or None if no tasks found\n # @PARAM resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE task is a valid Task object\n # @POST Returns resource name or task ID\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource name or fallback\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE task is a valid Task object\n # @POST Returns resource type or 'unknown'\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource type\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n# #endregion ResourceServiceModule\n" + "body": "# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset]\n# @BRIEF Shared service for fetching resource data with Git status and task status\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [TaskManagerPackage]\n# @RELATION DEPENDS_ON -> [TaskManagerModels]\n# @RELATION DEPENDS_ON -> [GitService]\n# @INVARIANT All resources include metadata about their current state\n# @SIDE_EFFECT Queries multiple backends for status\n# @DATA_CONTRACT ResourceQuery -> ResourceStatusSummary\n\nimport asyncio\nfrom datetime import UTC, datetime\nfrom typing import Any\n\nfrom ..core.logger import belief_scope, logger\nfrom ..core.superset_client import SupersetClient\nfrom ..core.task_manager.models import Task\nfrom ..services.git_service import GitService\n\n\n# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE None\n # @POST ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of dashboards with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE env is valid; page >= 1; page_size > 0.\n # @POST Returns page items plus total counters without scanning all pages locally.\n # @PARAM env (Environment) - Source environment.\n # @PARAM tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM page (int) - 1-based page number.\n # @PARAM page_size (int) - Page size.\n # @RETURN Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE dashboard_id is a valid integer identifier\n # @POST Returns the newest llm_dashboard_validation task summary or None\n # @PARAM dashboard_id (int) - The dashboard ID\n # @PARAM env_id (Optional[str]) - Environment ID to match task params\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n # @RELATION CALLS ->[EXT:method:_normalize_validation_status]\n # @RELATION CALLS ->[EXT:method:_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE raw_status can be enum or string\n # @POST Returns uppercase status without enum class prefix\n # @PARAM raw_status (Any) - Raw task status object/value\n # @RETURN str - Normalized status token\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE raw_status can be any scalar type\n # @POST Returns normalized validation status token or None\n # @PARAM raw_status (Any) - Raw validation status from task result\n # @RETURN Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE value may be datetime or any scalar.\n # @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM value (Any) - Candidate datetime-like value.\n # @RETURN datetime - UTC-aware comparable datetime.\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n # @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of datasets with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields\n # @RELATION CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]\n # @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]\n # @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database\n # StatsBar showed linked_count=0 because the field was never populated in list endpoint.\n # Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).\n # @REJECTED N+1 blocking calls rejected — would timeout at 26×1s. Semaphore + asyncio.to_thread + timeout prevents overload.\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status and linked dashboard count\n sem = asyncio.Semaphore(3) # limit concurrent Superset calls\n\n async def fetch_linked_count(ds_id: int) -> int:\n async with sem:\n try:\n return await asyncio.wait_for(\n asyncio.to_thread(\n client.get_dataset_linked_dashboard_count, ds_id\n ),\n timeout=10.0,\n )\n except (TimeoutError, Exception):\n logger.warning(\n f\"[EXT:method:get_datasets_with_status][Warning] \"\n f\"Failed to fetch linked dashboard count for dataset {ds_id}\"\n )\n return 0\n\n result = []\n linked_tasks = []\n for dataset in datasets:\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n linked_tasks.append(fetch_linked_count(dataset_id))\n\n # Fetch linked dashboard counts concurrently — FR-022\n if linked_tasks:\n linked_counts = await asyncio.gather(*linked_tasks)\n for ds, count in zip(result, linked_counts):\n ds['linked_dashboard_count'] = count\n\n logger.info(\n f\"[ResourceService][Coherence:OK] \"\n f\"Fetched {len(result)} datasets with status and linked counts\"\n )\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE tasks is a list of Task objects\n # @POST Returns summary with active_count and recent_tasks\n # @PARAM tasks (List[Task]) - List of tasks to summarize\n # @RETURN Dict - Activity summary\n # @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]\n # @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE dashboard_id is a valid integer\n # @POST Returns git status or None if no repo exists\n # @PARAM dashboard_id (int) - The dashboard ID\n # @RETURN Optional[Dict] - Git status with branch and sync_status\n # @RELATION CALLS ->[EXT:method:get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE resource_id is a valid string\n # @POST Returns task summary or None if no tasks found\n # @PARAM resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE task is a valid Task object\n # @POST Returns resource name or task ID\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource name or fallback\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE task is a valid Task object\n # @POST Returns resource type or 'unknown'\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource type\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n# #endregion ResourceServiceModule\n" }, { "contract_id": "ResourceService", @@ -48762,7 +49642,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE None\n # @POST ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of dashboards with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE env is valid; page >= 1; page_size > 0.\n # @POST Returns page items plus total counters without scanning all pages locally.\n # @PARAM env (Environment) - Source environment.\n # @PARAM tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM page (int) - 1-based page number.\n # @PARAM page_size (int) - Page size.\n # @RETURN Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE dashboard_id is a valid integer identifier\n # @POST Returns the newest llm_dashboard_validation task summary or None\n # @PARAM dashboard_id (int) - The dashboard ID\n # @PARAM env_id (Optional[str]) - Environment ID to match task params\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n # @RELATION CALLS ->[EXT:method:_normalize_validation_status]\n # @RELATION CALLS ->[EXT:method:_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE raw_status can be enum or string\n # @POST Returns uppercase status without enum class prefix\n # @PARAM raw_status (Any) - Raw task status object/value\n # @RETURN str - Normalized status token\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE raw_status can be any scalar type\n # @POST Returns normalized validation status token or None\n # @PARAM raw_status (Any) - Raw validation status from task result\n # @RETURN Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE value may be datetime or any scalar.\n # @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM value (Any) - Candidate datetime-like value.\n # @RETURN datetime - UTC-aware comparable datetime.\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n # @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of datasets with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields\n # @RELATION CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]\n # @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]\n # @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database\n # StatsBar showed linked_count=0 because the field was never populated in list endpoint.\n # Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).\n # @REJECTED N+1 blocking calls rejected — would timeout at 26×1s. Semaphore + asyncio.to_thread + timeout prevents overload.\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status and linked dashboard count\n sem = asyncio.Semaphore(3) # limit concurrent Superset calls\n\n async def fetch_linked_count(ds_id: int) -> int:\n async with sem:\n try:\n return await asyncio.wait_for(\n asyncio.to_thread(\n client.get_dataset_linked_dashboard_count, ds_id\n ),\n timeout=10.0,\n )\n except (asyncio.TimeoutError, Exception):\n logger.warning(\n f\"[EXT:method:get_datasets_with_status][Warning] \"\n f\"Failed to fetch linked dashboard count for dataset {ds_id}\"\n )\n return 0\n\n result = []\n linked_tasks = []\n for dataset in datasets:\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n linked_tasks.append(fetch_linked_count(dataset_id))\n\n # Fetch linked dashboard counts concurrently — FR-022\n if linked_tasks:\n linked_counts = await asyncio.gather(*linked_tasks)\n for ds, count in zip(result, linked_counts):\n ds['linked_dashboard_count'] = count\n\n logger.info(\n f\"[ResourceService][Coherence:OK] \"\n f\"Fetched {len(result)} datasets with status and linked counts\"\n )\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE tasks is a list of Task objects\n # @POST Returns summary with active_count and recent_tasks\n # @PARAM tasks (List[Task]) - List of tasks to summarize\n # @RETURN Dict - Activity summary\n # @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]\n # @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE dashboard_id is a valid integer\n # @POST Returns git status or None if no repo exists\n # @PARAM dashboard_id (int) - The dashboard ID\n # @RETURN Optional[Dict] - Git status with branch and sync_status\n # @RELATION CALLS ->[EXT:method:get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE resource_id is a valid string\n # @POST Returns task summary or None if no tasks found\n # @PARAM resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE task is a valid Task object\n # @POST Returns resource name or task ID\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource name or fallback\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE task is a valid Task object\n # @POST Returns resource type or 'unknown'\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource type\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n" + "body": "# #region ResourceService [C:3] [TYPE Class]\n# @BRIEF Provides centralized access to resource data with enhanced metadata\n# @RELATION DEPENDS_ON -> [SupersetClient]\n# @RELATION DEPENDS_ON -> [GitService]\nclass ResourceService:\n\n # region ResourceService_init [TYPE Function]\n # @PURPOSE: Initialize the resource service with dependencies\n # @PRE None\n # @POST ResourceService is ready to fetch resources\n def __init__(self):\n with belief_scope(\"ResourceService.__init__\"):\n self.git_service = GitService()\n logger.reason(\"Initialized ResourceService\", extra={\"src\": \"ResourceService\"})\n # endregion ResourceService_init\n\n # region get_dashboards_with_status [TYPE Function]\n # @PURPOSE: Fetch dashboards from environment with Git status and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of dashboards with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Dashboards with git_status and last_task fields\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummary]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_dashboards_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n dashboards = client.get_dashboards_summary(require_slug=require_slug)\n\n # Enhance each dashboard with Git status and task status\n result = []\n for dashboard in dashboards:\n # dashboard is already a dict, no need to call .dict()\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get('id')\n\n # Git status can be skipped for list endpoints and loaded lazily on UI side.\n if include_git_status:\n git_status = self._get_git_status_for_dashboard(dashboard_id)\n dashboard_dict['git_status'] = git_status\n else:\n dashboard_dict['git_status'] = None\n\n # Show status of the latest LLM validation for this dashboard.\n last_task = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n dashboard_dict['last_task'] = last_task\n\n result.append(dashboard_dict)\n\n logger.info(f\"[ResourceService][Coherence:OK] Fetched {len(result)} dashboards with status\")\n return result\n # endregion get_dashboards_with_status\n\n # region get_dashboards_page_with_status [TYPE Function]\n # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.\n # @PRE env is valid; page >= 1; page_size > 0.\n # @POST Returns page items plus total counters without scanning all pages locally.\n # @PARAM env (Environment) - Source environment.\n # @PARAM tasks (Optional[List[Task]]) - Tasks for latest LLM status.\n # @PARAM page (int) - 1-based page number.\n # @PARAM page_size (int) - Page size.\n # @RETURN Dict[str, Any] - {\"dashboards\": List[Dict], \"total\": int, \"total_pages\": int}\n # @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]\n # @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]\n # @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]\n async def get_dashboards_page_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None,\n page: int = 1,\n page_size: int = 10,\n search: str | None = None,\n include_git_status: bool = True,\n require_slug: bool = False,\n ) -> dict[str, Any]:\n with belief_scope(\n \"get_dashboards_page_with_status\",\n f\"env={env.id}, page={page}, page_size={page_size}, search={search}\",\n ):\n client = SupersetClient(env)\n total, dashboards_page = client.get_dashboards_summary_page(\n page=page,\n page_size=page_size,\n search=search,\n require_slug=require_slug,\n )\n\n result = []\n for dashboard in dashboards_page:\n dashboard_dict = dashboard\n dashboard_id = dashboard_dict.get(\"id\")\n\n if include_git_status:\n dashboard_dict[\"git_status\"] = self._get_git_status_for_dashboard(dashboard_id)\n else:\n dashboard_dict[\"git_status\"] = None\n\n dashboard_dict[\"last_task\"] = self._get_last_llm_task_for_dashboard(\n dashboard_id,\n env.id,\n tasks,\n )\n result.append(dashboard_dict)\n\n total_pages = (total + page_size - 1) // page_size if total > 0 else 1\n logger.info(\n \"[ResourceService][Coherence:OK] Fetched dashboards page %s/%s (%s items, total=%s)\",\n page,\n total_pages,\n len(result),\n total,\n )\n return {\n \"dashboards\": result,\n \"total\": total,\n \"total_pages\": total_pages,\n }\n # endregion get_dashboards_page_with_status\n\n # region _get_last_llm_task_for_dashboard [TYPE Function]\n # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment\n # @PRE dashboard_id is a valid integer identifier\n # @POST Returns the newest llm_dashboard_validation task summary or None\n # @PARAM dashboard_id (int) - The dashboard ID\n # @PARAM env_id (Optional[str]) - Environment ID to match task params\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n # @RELATION CALLS ->[EXT:method:_normalize_validation_status]\n # @RELATION CALLS ->[EXT:method:_normalize_task_status]\n def _get_last_llm_task_for_dashboard(\n self,\n dashboard_id: int,\n env_id: str | None,\n tasks: list[Task] | None = None,\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n dashboard_id_str = str(dashboard_id)\n matched_tasks = []\n\n for task in tasks:\n if getattr(task, \"plugin_id\", None) != \"llm_dashboard_validation\":\n continue\n\n params = getattr(task, \"params\", {}) or {}\n if str(params.get(\"dashboard_id\")) != dashboard_id_str:\n continue\n\n if env_id is not None:\n task_env = params.get(\"environment_id\") or params.get(\"env\")\n if str(task_env) != str(env_id):\n continue\n\n matched_tasks.append(task)\n\n if not matched_tasks:\n return None\n\n def _task_time(task_obj: Any) -> datetime:\n raw_time = (\n getattr(task_obj, \"started_at\", None)\n or getattr(task_obj, \"finished_at\", None)\n or getattr(task_obj, \"created_at\", None)\n )\n return self._normalize_datetime_for_compare(raw_time)\n\n projected_tasks = []\n for task in matched_tasks:\n raw_result = getattr(task, \"result\", None)\n validation_status = None\n if isinstance(raw_result, dict):\n validation_status = self._normalize_validation_status(raw_result.get(\"status\"))\n projected_tasks.append(\n (\n task,\n validation_status,\n _task_time(task),\n )\n )\n\n projected_tasks.sort(key=lambda item: item[2], reverse=True)\n latest_task, latest_validation_status, _ = projected_tasks[0]\n decisive_task = next(\n (\n item for item in projected_tasks\n if item[1] in {\"PASS\", \"WARN\", \"FAIL\"}\n ),\n None,\n )\n validation_status = latest_validation_status\n if validation_status == \"UNKNOWN\" and decisive_task is not None:\n validation_status = decisive_task[1]\n\n return {\n \"task_id\": str(getattr(latest_task, \"id\", \"\")),\n \"status\": self._normalize_task_status(getattr(latest_task, \"status\", \"\")),\n \"validation_status\": validation_status,\n }\n # endregion _get_last_llm_task_for_dashboard\n\n # region _normalize_task_status [TYPE Function]\n # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections\n # @PRE raw_status can be enum or string\n # @POST Returns uppercase status without enum class prefix\n # @PARAM raw_status (Any) - Raw task status object/value\n # @RETURN str - Normalized status token\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_task_status(self, raw_status: Any) -> str:\n if raw_status is None:\n return \"\"\n value = getattr(raw_status, \"value\", raw_status)\n status_text = str(value).strip()\n if \".\" in status_text:\n status_text = status_text.split(\".\")[-1]\n return status_text.upper()\n # endregion _normalize_task_status\n\n # region _normalize_validation_status [TYPE Function]\n # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN\n # @PRE raw_status can be any scalar type\n # @POST Returns normalized validation status token or None\n # @PARAM raw_status (Any) - Raw validation status from task result\n # @RETURN Optional[str] - PASS|FAIL|WARN|UNKNOWN\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n def _normalize_validation_status(self, raw_status: Any) -> str | None:\n if raw_status is None:\n return None\n status_text = str(raw_status).strip().upper()\n if status_text in {\"PASS\", \"FAIL\", \"WARN\"}:\n return status_text\n return \"UNKNOWN\"\n # endregion _normalize_validation_status\n\n # region _normalize_datetime_for_compare [TYPE Function]\n # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.\n # @PRE value may be datetime or any scalar.\n # @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.\n # @PARAM value (Any) - Candidate datetime-like value.\n # @RETURN datetime - UTC-aware comparable datetime.\n # @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]\n # @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]\n def _normalize_datetime_for_compare(self, value: Any) -> datetime:\n if isinstance(value, datetime):\n if value.tzinfo is None:\n return value.replace(tzinfo=UTC)\n return value.astimezone(UTC)\n return datetime.min.replace(tzinfo=UTC)\n # endregion _normalize_datetime_for_compare\n\n # region get_datasets_with_status [TYPE Function]\n # @PURPOSE: Fetch datasets from environment with mapping progress and last task status\n # @PRE env is a valid Environment object\n # @POST Returns list of datasets with enhanced metadata\n # @PARAM env (Environment) - The environment to fetch from\n # @PARAM tasks (List[Task]) - List of tasks to check for status\n # @RETURN List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields\n # @RELATION CALLS -> [SupersetClientGetDatasetsSummary]\n # @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]\n # @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]\n # @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database\n # StatsBar showed linked_count=0 because the field was never populated in list endpoint.\n # Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).\n # @REJECTED N+1 blocking calls rejected — would timeout at 26×1s. Semaphore + asyncio.to_thread + timeout prevents overload.\n async def get_datasets_with_status(\n self,\n env: Any,\n tasks: list[Task] | None = None\n ) -> list[dict[str, Any]]:\n with belief_scope(\"get_datasets_with_status\", f\"env={env.id}\"):\n client = SupersetClient(env)\n datasets = client.get_datasets_summary()\n\n # Enhance each dataset with task status and linked dashboard count\n sem = asyncio.Semaphore(3) # limit concurrent Superset calls\n\n async def fetch_linked_count(ds_id: int) -> int:\n async with sem:\n try:\n return await asyncio.wait_for(\n asyncio.to_thread(\n client.get_dataset_linked_dashboard_count, ds_id\n ),\n timeout=10.0,\n )\n except (TimeoutError, Exception):\n logger.warning(\n f\"[EXT:method:get_datasets_with_status][Warning] \"\n f\"Failed to fetch linked dashboard count for dataset {ds_id}\"\n )\n return 0\n\n result = []\n linked_tasks = []\n for dataset in datasets:\n dataset_dict = dataset\n dataset_id = dataset_dict.get('id')\n\n # Get last task status\n last_task = self._get_last_task_for_resource(\n f\"dataset-{dataset_id}\",\n tasks\n )\n dataset_dict['last_task'] = last_task\n\n result.append(dataset_dict)\n linked_tasks.append(fetch_linked_count(dataset_id))\n\n # Fetch linked dashboard counts concurrently — FR-022\n if linked_tasks:\n linked_counts = await asyncio.gather(*linked_tasks)\n for ds, count in zip(result, linked_counts):\n ds['linked_dashboard_count'] = count\n\n logger.info(\n f\"[ResourceService][Coherence:OK] \"\n f\"Fetched {len(result)} datasets with status and linked counts\"\n )\n return result\n # endregion get_datasets_with_status\n\n # region get_activity_summary [TYPE Function]\n # @PURPOSE: Get summary of active and recent tasks for the activity indicator\n # @PRE tasks is a list of Task objects\n # @POST Returns summary with active_count and recent_tasks\n # @PARAM tasks (List[Task]) - List of tasks to summarize\n # @RETURN Dict - Activity summary\n # @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]\n # @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]\n def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:\n with belief_scope(\"get_activity_summary\"):\n # Count active (RUNNING, WAITING_INPUT) tasks\n active_tasks = [\n t for t in tasks\n if t.status in ['RUNNING', 'WAITING_INPUT']\n ]\n\n # Get recent tasks (last 5)\n recent_tasks = sorted(\n tasks,\n key=lambda t: t.created_at,\n reverse=True\n )[:5]\n\n # Format recent tasks for frontend\n recent_tasks_formatted = []\n for task in recent_tasks:\n resource_name = self._extract_resource_name_from_task(task)\n recent_tasks_formatted.append({\n 'task_id': str(task.id),\n 'resource_name': resource_name,\n 'resource_type': self._extract_resource_type_from_task(task),\n 'status': task.status,\n 'started_at': task.created_at.isoformat() if task.created_at else None\n })\n\n return {\n 'active_count': len(active_tasks),\n 'recent_tasks': recent_tasks_formatted\n }\n # endregion get_activity_summary\n\n # region _get_git_status_for_dashboard [TYPE Function]\n # @PURPOSE: Get Git sync status for a dashboard\n # @PRE dashboard_id is a valid integer\n # @POST Returns git status or None if no repo exists\n # @PARAM dashboard_id (int) - The dashboard ID\n # @RETURN Optional[Dict] - Git status with branch and sync_status\n # @RELATION CALLS ->[EXT:method:get_repo]\n def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:\n try:\n repo = self.git_service.get_repo(dashboard_id)\n if not repo:\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n\n # Check if there are uncommitted changes\n try:\n # Get current branch\n branch = repo.active_branch.name\n\n # Check for uncommitted changes\n is_dirty = repo.is_dirty()\n has_changes_for_commit = repo.is_dirty(untracked_files=True)\n\n # Check for unpushed commits\n unpushed = len(list(repo.iter_commits(f'{branch}@{{u}}..{branch}'))) if '@{u}' in str(repo.refs) else 0\n\n if is_dirty or unpushed > 0:\n sync_status = 'DIFF'\n else:\n sync_status = 'OK'\n\n return {\n 'branch': branch,\n 'sync_status': sync_status,\n 'has_repo': True,\n 'has_changes_for_commit': has_changes_for_commit\n }\n except Exception:\n logger.warning(f\"[ResourceService][Warning] Failed to get git status for dashboard {dashboard_id}\")\n return {\n 'branch': None,\n 'sync_status': 'ERROR',\n 'has_repo': True,\n 'has_changes_for_commit': False\n }\n except Exception:\n # No repo exists for this dashboard\n return {\n 'branch': None,\n 'sync_status': 'NO_REPO',\n 'has_repo': False,\n 'has_changes_for_commit': False\n }\n # endregion _get_git_status_for_dashboard\n\n # region _get_last_task_for_resource [TYPE Function]\n # @PURPOSE: Get the most recent task for a specific resource\n # @PRE resource_id is a valid string\n # @POST Returns task summary or None if no tasks found\n # @PARAM resource_id (str) - The resource identifier (e.g., \"dashboard-123\")\n # @PARAM tasks (Optional[List[Task]]) - List of tasks to search\n # @RETURN Optional[Dict] - Task summary with task_id and status\n # @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]\n def _get_last_task_for_resource(\n self,\n resource_id: str,\n tasks: list[Task] | None = None\n ) -> dict[str, Any] | None:\n if not tasks:\n return None\n\n # Filter tasks for this resource\n resource_tasks = []\n for task in tasks:\n params = task.params or {}\n if params.get('resource_id') == resource_id:\n resource_tasks.append(task)\n\n if not resource_tasks:\n return None\n\n # Get most recent task with timezone-safe comparison.\n last_task = max(\n resource_tasks,\n key=lambda t: self._normalize_datetime_for_compare(getattr(t, \"created_at\", None)),\n )\n\n return {\n 'task_id': str(last_task.id),\n 'status': last_task.status\n }\n # endregion _get_last_task_for_resource\n\n # region _extract_resource_name_from_task [TYPE Function]\n # @PURPOSE: Extract resource name from task params\n # @PRE task is a valid Task object\n # @POST Returns resource name or task ID\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource name or fallback\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_name_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_name', f\"Task {task.id}\")\n # endregion _extract_resource_name_from_task\n\n # region _extract_resource_type_from_task [TYPE Function]\n # @PURPOSE: Extract resource type from task params\n # @PRE task is a valid Task object\n # @POST Returns resource type or 'unknown'\n # @PARAM task (Task) - The task to extract from\n # @RETURN str - Resource type\n # @RELATION CALLED_BY -> [EXT:method:get_activity_summary]\n def _extract_resource_type_from_task(self, task: Task) -> str:\n params = task.params or {}\n return params.get('resource_type', 'unknown')\n # endregion _extract_resource_type_from_task\n# #endregion ResourceService\n" }, { "contract_id": "security_badge_service", @@ -48887,7 +49767,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region SqlTableExtractorModule [C:2] [TYPE Module] [SEMANTICS sql, jinja, table, extraction, parsing]\n# @BRIEF Three-phase SQL+Jinja table name extractor for virtual datasets as per FR-003.\n# Phase 1: Detect Jinja spans vs SQL spans\n# Phase 2: In Jinja spans, extract \"schema.table\" from string values\n# Phase 3: In SQL spans, regex pattern + sqlparse filter to reject string literal false positives\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlparse]\n# @INVARIANT Only exact schema.table matches (case-insensitive); unqualified references are NOT matched.\n# @INVARIANT Returns a set[str] of fully-qualified table names (lowercased for case-insensitive matching).\n\nimport re\nfrom typing import Iterable\n\nimport sqlparse\nfrom sqlparse.sql import Identifier, Parenthesis, Token, TokenList\nfrom sqlparse.tokens import Literal, Name, Punctuation, Whitespace\n\n# ── Phase 1: Jinja span detection ───────────────────────────\n\n# Minimal Jinja token detection: {% ... %}, {{ ... }}, {# ... #}\n_JINJA_BLOCK_RE = re.compile(r\"{%-?\\s*.*?\\s*-?%}\", re.DOTALL)\n_JINJA_EXPR_RE = re.compile(r\"{{-?\\s*.*?\\s*-?}}\", re.DOTALL)\n_JINJA_COMMENT_RE = re.compile(r\"{#.*?#}\", re.DOTALL)\n\n# ── Phase 3: schema.table regex (case-insensitive) ───────────\n_SCHEMA_TABLE_RE = re.compile(\n r\"\"\"\n (? list[tuple[str, str]]:\n \"\"\"Split raw SQL+Jinja text into alternating Jinja and SQL spans.\n\n Phase 1 detects Jinja blocks ({%%}, {{}}, {##}) and returns the\n remaining text as SQL spans.\n \"\"\"\n if not raw_sql or not raw_sql.strip():\n return [(\"sql\", raw_sql or \"\")]\n\n spans: list[tuple[str, str]] = []\n # Collect all Jinja match positions\n jinja_matches: list[tuple[int, int]] = []\n for pattern in (_JINJA_BLOCK_RE, _JINJA_EXPR_RE, _JINJA_COMMENT_RE):\n for m in pattern.finditer(raw_sql):\n jinja_matches.append((m.start(), m.end()))\n\n # Merge overlapping Jinja spans\n if jinja_matches:\n jinja_matches.sort()\n merged: list[tuple[int, int]] = [jinja_matches[0]]\n for start, end in jinja_matches[1:]:\n if start <= merged[-1][1]:\n merged[-1] = (merged[-1][0], max(merged[-1][1], end))\n else:\n merged.append((start, end))\n\n # Build alternating sql/jinja spans\n cursor = 0\n for start, end in merged:\n if cursor < start:\n sql_chunk = raw_sql[cursor:start].strip()\n if sql_chunk:\n spans.append((\"sql\", sql_chunk))\n jinja_chunk = raw_sql[start:end].strip()\n if jinja_chunk:\n spans.append((\"jinja\", jinja_chunk))\n cursor = end\n if cursor < len(raw_sql):\n remaining = raw_sql[cursor:].strip()\n if remaining:\n spans.append((\"sql\", remaining))\n else:\n spans.append((\"sql\", raw_sql.strip()))\n\n return spans\n# #endregion detect_jinja_spans\n\n\n# #region extract_tables_from_jinja [C:2] [TYPE Function]\n# @BRIEF Phase 2: Extract \"schema.table\" references from Jinja string values.\n# Looks for patterns like \"raw.sales\" inside Jinja text.\n# @PRE jinja_text is a string from a Jinja span.\n# @POST Returns a set of lowercased schema.table strings.\ndef extract_tables_from_jinja(jinja_text: str) -> set[str]:\n \"\"\"Extract ``schema.table`` references from within Jinja template blocks.\n\n Looks for double-quoted or single-quoted string values that match\n the ``schema.table`` pattern, e.g. ``\"raw.sales\"`` or ``'raw.inventory'``.\n \"\"\"\n tables: set[str] = set()\n # Match string values: \"schema.table\" or 'schema.table'\n string_pattern = re.compile(\n r\"\"\"[\"']([a-zA-Z_][\\w]*\\.[a-zA-Z_][\\w]*)[\"']\"\"\"\n )\n for m in string_pattern.finditer(jinja_text):\n tables.add(m.group(1).lower())\n return tables\n# #endregion extract_tables_from_jinja\n\n\n# #region is_string_literal [C:1] [TYPE Function]\n# @BRIEF Check if a sqlparse Token is a string literal (not a schema.table identifier).\n# @PRE token is a sqlparse Token.\n# @POST Returns True if the token is a string/Single/Literal.String.\ndef is_string_literal(token: Token) -> bool:\n \"\"\"Return True if token is a string literal type in sqlparse.\"\"\"\n return token.ttype is Literal.String.Single or token.ttype is Literal.String\n# #endregion is_string_literal\n\n\n# #region extract_tables_from_sql_span [C:2] [TYPE Function]\n# @BRIEF Phase 3: Extract schema.table references from SQL text using regex + sqlparse filtering.\n# Uses regex to find all schema.table candidates, then sqlparse to filter out\n# false positives inside string literals.\n# @PRE sql_text is a string from a SQL span (non-Jinja).\n# @POST Returns a set of lowercased schema.table strings.\ndef extract_tables_from_sql_span(sql_text: str) -> set[str]:\n \"\"\"Extract ``schema.table`` references from plain SQL text.\n\n Uses regex to find all ``schema.table`` candidates, then sqlparse\n to reject matches that fall inside string literals.\n \"\"\"\n tables: set[str] = set()\n raw_matches = _SCHEMA_TABLE_RE.findall(sql_text)\n if not raw_matches:\n return tables\n\n # Use sqlparse to identify string literal positions\n parsed = sqlparse.parse(sql_text)\n string_literal_ranges: list[tuple[int, int]] = []\n\n def walk_tokens(tokens: Iterable[Token], base_offset: int = 0) -> None:\n offset = base_offset\n for token in tokens:\n if isinstance(token, TokenList):\n walk_tokens(token.flatten(), offset)\n else:\n ttype = token.ttype\n val = token.value\n if is_string_literal(token):\n string_literal_ranges.append(\n (offset, offset + len(val))\n )\n offset += len(val)\n\n for stmt in parsed:\n if stmt is None:\n continue\n walk_tokens(stmt.flatten(), base_offset=0)\n\n def is_in_string(pos: int) -> bool:\n for s_start, s_end in string_literal_ranges:\n if s_start <= pos <= s_end:\n return True\n return False\n\n # Re-scan with full match positions to filter\n for m in _SCHEMA_TABLE_RE.finditer(sql_text):\n if not is_in_string(m.start()):\n # Extract schema and table from match groups\n schema = m.group(2) or m.group(1) or \"\"\n table = m.group(3) or \"\"\n # Filter out column aliases (single-char schemas like \"a.id\", \"o.id\")\n if len(schema) < 2 and schema.isalpha() and schema.islower():\n continue\n if schema and table:\n tables.add(f\"{schema.lower()}.{table.lower()}\")\n\n return tables\n# #endregion extract_tables_from_sql_span\n\n\n# #region extract_tables_from_sql [C:2] [TYPE Function]\n# @BRIEF Three-phase entry point: extract all schema.table references from raw SQL+Jinja text.\n# @PRE raw_sql is a string (possibly empty).\n# @POST Returns a set of lowercased fully-qualified table names (schema.table format).\n# @RELATION CALLS -> [detect_jinja_spans]\n# @RELATION CALLS -> [extract_tables_from_jinja]\n# @RELATION CALLS -> [extract_tables_from_sql_span]\ndef extract_tables_from_sql(raw_sql: str) -> set[str]:\n \"\"\"Extract all ``schema.table`` references from raw SQL + Jinja text.\n\n Three-phase approach per FR-003:\n 1. Detect Jinja spans vs SQL spans\n 2. In Jinja spans, extract ``schema.table`` from quoted string values\n 3. In SQL spans, regex + sqlparse filter to reject string literal false positives\n\n Returns a set of lowercased ``schema.table`` strings for case-insensitive matching.\n\n Example:\n >>> extract_tables_from_sql(\"SELECT * FROM raw.sales JOIN raw.inventory ON ...\")\n {'raw.sales', 'raw.inventory'}\n \"\"\"\n if not raw_sql or not raw_sql.strip():\n return set()\n\n all_tables: set[str] = set()\n spans = detect_jinja_spans(raw_sql)\n\n for span_type, text in spans:\n if span_type == \"jinja\":\n all_tables.update(extract_tables_from_jinja(text))\n else:\n all_tables.update(extract_tables_from_sql_span(text))\n\n return all_tables\n# #endregion extract_tables_from_sql\n\n# #endregion SqlTableExtractorModule\n" + "body": "# #region SqlTableExtractorModule [C:2] [TYPE Module] [SEMANTICS sql, jinja, table, extraction, parsing]\n# @BRIEF Three-phase SQL+Jinja table name extractor for virtual datasets as per FR-003.\n# Phase 1: Detect Jinja spans vs SQL spans\n# Phase 2: In Jinja spans, extract \"schema.table\" from string values\n# Phase 3: In SQL spans, regex pattern + sqlparse filter to reject string literal false positives\n# @LAYER Service\n# @RELATION DEPENDS_ON -> [EXT:Library:sqlparse]\n# @INVARIANT Only exact schema.table matches (case-insensitive); unqualified references are NOT matched.\n# @INVARIANT Returns a set[str] of fully-qualified table names (lowercased for case-insensitive matching).\n\nfrom collections.abc import Iterable\nimport re\n\nimport sqlparse\nfrom sqlparse.sql import Token, TokenList\nfrom sqlparse.tokens import Literal\n\n# ── Phase 1: Jinja span detection ───────────────────────────\n\n# Minimal Jinja token detection: {% ... %}, {{ ... }}, {# ... #}\n_JINJA_BLOCK_RE = re.compile(r\"{%-?\\s*.*?\\s*-?%}\", re.DOTALL)\n_JINJA_EXPR_RE = re.compile(r\"{{-?\\s*.*?\\s*-?}}\", re.DOTALL)\n_JINJA_COMMENT_RE = re.compile(r\"{#.*?#}\", re.DOTALL)\n\n# ── Phase 3: schema.table regex (case-insensitive) ───────────\n_SCHEMA_TABLE_RE = re.compile(\n r\"\"\"\n (? list[tuple[str, str]]:\n \"\"\"Split raw SQL+Jinja text into alternating Jinja and SQL spans.\n\n Phase 1 detects Jinja blocks ({%%}, {{}}, {##}) and returns the\n remaining text as SQL spans.\n \"\"\"\n if not raw_sql or not raw_sql.strip():\n return [(\"sql\", raw_sql or \"\")]\n\n spans: list[tuple[str, str]] = []\n # Collect all Jinja match positions\n jinja_matches: list[tuple[int, int]] = []\n for pattern in (_JINJA_BLOCK_RE, _JINJA_EXPR_RE, _JINJA_COMMENT_RE):\n for m in pattern.finditer(raw_sql):\n jinja_matches.append((m.start(), m.end()))\n\n # Merge overlapping Jinja spans\n if jinja_matches:\n jinja_matches.sort()\n merged: list[tuple[int, int]] = [jinja_matches[0]]\n for start, end in jinja_matches[1:]:\n if start <= merged[-1][1]:\n merged[-1] = (merged[-1][0], max(merged[-1][1], end))\n else:\n merged.append((start, end))\n\n # Build alternating sql/jinja spans\n cursor = 0\n for start, end in merged:\n if cursor < start:\n sql_chunk = raw_sql[cursor:start].strip()\n if sql_chunk:\n spans.append((\"sql\", sql_chunk))\n jinja_chunk = raw_sql[start:end].strip()\n if jinja_chunk:\n spans.append((\"jinja\", jinja_chunk))\n cursor = end\n if cursor < len(raw_sql):\n remaining = raw_sql[cursor:].strip()\n if remaining:\n spans.append((\"sql\", remaining))\n else:\n spans.append((\"sql\", raw_sql.strip()))\n\n return spans\n# #endregion detect_jinja_spans\n\n\n# #region extract_tables_from_jinja [C:2] [TYPE Function]\n# @BRIEF Phase 2: Extract \"schema.table\" references from Jinja string values.\n# Looks for patterns like \"raw.sales\" inside Jinja text.\n# @PRE jinja_text is a string from a Jinja span.\n# @POST Returns a set of lowercased schema.table strings.\ndef extract_tables_from_jinja(jinja_text: str) -> set[str]:\n \"\"\"Extract ``schema.table`` references from within Jinja template blocks.\n\n Looks for double-quoted or single-quoted string values that match\n the ``schema.table`` pattern, e.g. ``\"raw.sales\"`` or ``'raw.inventory'``.\n \"\"\"\n tables: set[str] = set()\n # Match string values: \"schema.table\" or 'schema.table'\n string_pattern = re.compile(\n r\"\"\"[\"']([a-zA-Z_][\\w]*\\.[a-zA-Z_][\\w]*)[\"']\"\"\"\n )\n for m in string_pattern.finditer(jinja_text):\n tables.add(m.group(1).lower())\n return tables\n# #endregion extract_tables_from_jinja\n\n\n# #region is_string_literal [C:1] [TYPE Function]\n# @BRIEF Check if a sqlparse Token is a string literal (not a schema.table identifier).\n# @PRE token is a sqlparse Token.\n# @POST Returns True if the token is a string/Single/Literal.String.\ndef is_string_literal(token: Token) -> bool:\n \"\"\"Return True if token is a string literal type in sqlparse.\"\"\"\n return token.ttype is Literal.String.Single or token.ttype is Literal.String\n# #endregion is_string_literal\n\n\n# #region extract_tables_from_sql_span [C:2] [TYPE Function]\n# @BRIEF Phase 3: Extract schema.table references from SQL text using regex + sqlparse filtering.\n# Uses regex to find all schema.table candidates, then sqlparse to filter out\n# false positives inside string literals.\n# @PRE sql_text is a string from a SQL span (non-Jinja).\n# @POST Returns a set of lowercased schema.table strings.\ndef extract_tables_from_sql_span(sql_text: str) -> set[str]:\n \"\"\"Extract ``schema.table`` references from plain SQL text.\n\n Uses regex to find all ``schema.table`` candidates, then sqlparse\n to reject matches that fall inside string literals.\n \"\"\"\n tables: set[str] = set()\n raw_matches = _SCHEMA_TABLE_RE.findall(sql_text)\n if not raw_matches:\n return tables\n\n # Use sqlparse to identify string literal positions\n parsed = sqlparse.parse(sql_text)\n string_literal_ranges: list[tuple[int, int]] = []\n\n def walk_tokens(tokens: Iterable[Token], base_offset: int = 0) -> None:\n offset = base_offset\n for token in tokens:\n if isinstance(token, TokenList):\n walk_tokens(token.flatten(), offset)\n else:\n ttype = token.ttype\n val = token.value\n if is_string_literal(token):\n string_literal_ranges.append(\n (offset, offset + len(val))\n )\n offset += len(val)\n\n for stmt in parsed:\n if stmt is None:\n continue\n walk_tokens(stmt.flatten(), base_offset=0)\n\n def is_in_string(pos: int) -> bool:\n for s_start, s_end in string_literal_ranges:\n if s_start <= pos <= s_end:\n return True\n return False\n\n # Re-scan with full match positions to filter\n for m in _SCHEMA_TABLE_RE.finditer(sql_text):\n if not is_in_string(m.start()):\n # Extract schema and table from match groups\n schema = m.group(2) or m.group(1) or \"\"\n table = m.group(3) or \"\"\n # Filter out column aliases (single-char schemas like \"a.id\", \"o.id\")\n if len(schema) < 2 and schema.isalpha() and schema.islower():\n continue\n if schema and table:\n tables.add(f\"{schema.lower()}.{table.lower()}\")\n\n return tables\n# #endregion extract_tables_from_sql_span\n\n\n# #region extract_tables_from_sql [C:2] [TYPE Function]\n# @BRIEF Three-phase entry point: extract all schema.table references from raw SQL+Jinja text.\n# @PRE raw_sql is a string (possibly empty).\n# @POST Returns a set of lowercased fully-qualified table names (schema.table format).\n# @RELATION CALLS -> [detect_jinja_spans]\n# @RELATION CALLS -> [extract_tables_from_jinja]\n# @RELATION CALLS -> [extract_tables_from_sql_span]\ndef extract_tables_from_sql(raw_sql: str) -> set[str]:\n \"\"\"Extract all ``schema.table`` references from raw SQL + Jinja text.\n\n Three-phase approach per FR-003:\n 1. Detect Jinja spans vs SQL spans\n 2. In Jinja spans, extract ``schema.table`` from quoted string values\n 3. In SQL spans, regex + sqlparse filter to reject string literal false positives\n\n Returns a set of lowercased ``schema.table`` strings for case-insensitive matching.\n\n Example:\n >>> extract_tables_from_sql(\"SELECT * FROM raw.sales JOIN raw.inventory ON ...\")\n {'raw.sales', 'raw.inventory'}\n \"\"\"\n if not raw_sql or not raw_sql.strip():\n return set()\n\n all_tables: set[str] = set()\n spans = detect_jinja_spans(raw_sql)\n\n for span_type, text in spans:\n if span_type == \"jinja\":\n all_tables.update(extract_tables_from_jinja(text))\n else:\n all_tables.update(extract_tables_from_sql_span(text))\n\n return all_tables\n# #endregion extract_tables_from_sql\n\n# #endregion SqlTableExtractorModule\n" }, { "contract_id": "detect_jinja_spans", @@ -49134,7 +50014,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationService [C:3] [TYPE Module] [SEMANTICS validation, service, sqlalchemy, task]\n# @BRIEF Business logic for validation task CRUD and trigger-run.\n# @LAYER Service\n\nfrom datetime import time\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import logger\nfrom ..core.task_manager import TaskManager\nfrom ..models.llm import LLMProvider, ValidationPolicy, ValidationRecord\nfrom ..schemas.validation import ValidationTaskCreate, ValidationTaskResponse, ValidationTaskUpdate\nfrom .llm_provider import LLMProviderService\n\n\ndef _parse_time(value: str | None) -> time | None:\n \"\"\"Parse HH:MM string to datetime.time, or return None.\"\"\"\n if not value:\n return None\n try:\n parts = value.strip().split(\":\")\n return time(int(parts[0]), int(parts[1]))\n except (ValueError, IndexError):\n return None\n\n\ndef _format_time(value: time | None) -> str | None:\n \"\"\"Format datetime.time to HH:MM string, or return None.\"\"\"\n if value is None:\n return None\n return f\"{value.hour:02d}:{value.minute:02d}\"\n\n\n# #region ValidationTaskService [C:4] [TYPE Class]\n# @BRIEF Validation task (policy) CRUD with provider validation and trigger-run.\n# @RATIONALE Service-layer class for validation task CRUD with provider validation and trigger-run. Separates API layer concerns from business logic; enables unit testing without HTTP.\n# @REJECTED Embedding CRUD in route handlers rejected — would duplicate validation logic across endpoints; centralized service ensures consistent provider/environment validation before persistence.\nclass ValidationTaskService:\n\n def __init__(self, db: Session, config_manager=None, username: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.username = username or \"system\"\n\n # #region _validate_provider [C:2] [TYPE Function]\n # @RATIONALE Centralizes multimodal LLM provider check so all task operations (create, update, trigger) share the same validation gate. Uses LLMProviderService for reuse.\n # @REJECTED Duplicating multimodal check in each caller rejected — would create maintenance burden if provider validation rules change or error messages need updating.\n def _validate_provider(self, provider_id: str) -> LLMProvider:\n if not provider_id or not provider_id.strip():\n raise ValueError(\"provider_id is required\")\n svc = LLMProviderService(self.db)\n provider = svc.get_provider(provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{provider_id}' not found\")\n if not bool(provider.is_multimodal):\n raise ValueError(\n f\"LLM provider '{provider.name}' is not multimodal. \"\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n return provider\n\n # #endregion _validate_provider\n\n # #region _validate_environment [C:1] [TYPE Function]\n def _validate_environment(self, environment_id: str) -> None:\n if not self.config_manager:\n return\n env = self.config_manager.get_environment(environment_id)\n if not env:\n raise ValueError(f\"Environment '{environment_id}' not found\")\n\n # #endregion _validate_environment\n\n # #region _policy_to_response [C:2] [TYPE Function]\n def _policy_to_response(self, policy: ValidationPolicy) -> ValidationTaskResponse:\n last_run = None\n if policy.dashboard_ids:\n last_run = (\n self.db.query(ValidationRecord)\n .filter(\n ValidationRecord.dashboard_id.in_(policy.dashboard_ids),\n ValidationRecord.environment_id == policy.environment_id,\n )\n .order_by(ValidationRecord.timestamp.desc())\n .first()\n )\n return ValidationTaskResponse(\n id=policy.id,\n name=policy.name,\n environment_id=policy.environment_id,\n is_active=bool(policy.is_active) if policy.is_active is not None else True,\n dashboard_ids=list(policy.dashboard_ids) if policy.dashboard_ids else [],\n provider_id=policy.provider_id,\n schedule_days=list(policy.schedule_days) if policy.schedule_days else None,\n window_start=_format_time(policy.window_start),\n window_end=_format_time(policy.window_end),\n notify_owners=bool(policy.notify_owners) if policy.notify_owners is not None else True,\n custom_channels=list(policy.custom_channels) if policy.custom_channels else None,\n alert_condition=policy.alert_condition or \"FAIL_ONLY\",\n created_at=policy.created_at,\n updated_at=policy.updated_at,\n last_run_status=str(last_run.status) if last_run else None,\n last_run_at=last_run.timestamp if last_run else None,\n )\n\n # #endregion _policy_to_response\n\n # #region list_tasks [C:3] [TYPE Function]\n # @RATIONALE Uses query-time filtering with optional is_active/environment_id params rather than post-filtering results in memory, reducing overhead with large task counts.\n # @REJECTED In-memory filtering after loading all rows rejected — would be inefficient with hundreds of tasks; database-level filtering scales better.\n def list_tasks(\n self,\n is_active: bool | None = None,\n environment_id: str | None = None,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[ValidationTaskResponse]]:\n query = self.db.query(ValidationPolicy)\n if is_active is not None:\n query = query.filter(ValidationPolicy.is_active == is_active)\n if environment_id:\n query = query.filter(ValidationPolicy.environment_id == environment_id)\n\n total = query.count()\n policies = (\n query.order_by(ValidationPolicy.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return total, [self._policy_to_response(p) for p in policies]\n\n # #endregion list_tasks\n\n # #region create_task [C:4] [TYPE Function]\n # @RATIONALE Validates provider (multimodal check) and environment before persisting the policy, ensuring referential integrity at creation time and preventing silent runtime failures.\n # @REJECTED Lazy validation on first run rejected — would allow creation of invalid task configurations with non-multimodal providers that fail only at runtime during dashboard validation.\n def create_task(self, payload: ValidationTaskCreate) -> ValidationTaskResponse:\n self._validate_provider(payload.provider_id)\n self._validate_environment(payload.environment_id)\n\n ws = _parse_time(payload.window_start)\n we = _parse_time(payload.window_end)\n\n policy = ValidationPolicy(\n name=payload.name,\n environment_id=payload.environment_id,\n is_active=payload.is_active,\n dashboard_ids=payload.dashboard_ids or [],\n provider_id=payload.provider_id,\n schedule_days=payload.schedule_days or [],\n window_start=ws or time(9, 0),\n window_end=we or time(18, 0),\n notify_owners=payload.notify_owners,\n custom_channels=payload.custom_channels,\n alert_condition=payload.alert_condition or \"FAIL_ONLY\",\n )\n self.db.add(policy)\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\n \"[ValidationTaskService] Created task '%s' (id=%s)\", policy.name, policy.id\n )\n return self._policy_to_response(policy)\n\n # #endregion create_task\n\n # #region get_task [C:3] [TYPE Function]\n # @RATIONALE Returns full task response including last_run_status via joined subquery — gives the UI immediate visibility into task health without a second API call.\n # @REJECTED Separate query for last run status rejected — double-query pattern would add latency on every task detail request.\n def get_task(self, task_id: str) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n return self._policy_to_response(policy)\n\n # #endregion get_task\n\n # #region update_task [C:4] [TYPE Function]\n # @RATIONALE Re-validates provider/environment only when those fields change (via exclude_unset), avoiding unnecessary DB calls on partial updates and allowing other fields to be updated independently.\n # @REJECTED Full re-validation on every update rejected — would prevent updating non-critical fields (e.g., schedule, name) when a provider is temporarily unavailable.\n def update_task(self, task_id: str, payload: ValidationTaskUpdate) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n update_data = payload.model_dump(exclude_unset=True)\n\n if \"provider_id\" in update_data and update_data[\"provider_id\"]:\n self._validate_provider(update_data[\"provider_id\"])\n if \"environment_id\" in update_data and update_data[\"environment_id\"]:\n self._validate_environment(update_data[\"environment_id\"])\n\n if \"window_start\" in update_data:\n ws = _parse_time(update_data.pop(\"window_start\", None))\n if ws is not None:\n policy.window_start = ws\n if \"window_end\" in update_data:\n we = _parse_time(update_data.pop(\"window_end\", None))\n if we is not None:\n policy.window_end = we\n\n for key, value in update_data.items():\n if hasattr(policy, key):\n setattr(policy, key, value)\n\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\"[ValidationTaskService] Updated task '%s' (id=%s)\", policy.name, policy.id)\n return self._policy_to_response(policy)\n\n # #endregion update_task\n\n # #region delete_task [C:3] [TYPE Function]\n# @RATIONALE Scope run deletion by policy_id instead of dashboard_id+environment_id to prevent deleting runs belonging to other tasks that validate the same dashboard. Requires policy_id column on ValidationRecord, set by the plugin during execution.\n# @REJECTED Always-cascade delete rejected – would destroy run history unexpectedly. Scoping by dashboard_id+environment_id rejected – caused cross-task data loss (F4). Explicit delete_runs flag with policy_id scoping gives users safe control.\n\n def delete_task(self, task_id: str, delete_runs: bool = False) -> None:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n if delete_runs:\n self.db.query(ValidationRecord).filter(\n ValidationRecord.policy_id == policy.id,\n ).delete(synchronize_session=False)\n\n self.db.delete(policy)\n self.db.commit()\n logger.info(\n \"[ValidationTaskService] Deleted task '%s' (id=%s)\", policy.name, policy.id\n )\n\n # #endregion delete_task\n # #region trigger_run [C:4] [TYPE Function]\n # @RATIONALE Uses TaskManager.create_task() to spawn validation via the llm_dashboard_validation plugin — reuses existing scheduling, persistence, retry, and notification infrastructure.\n # @REJECTED Direct LLM provider call from service layer rejected — would bypass TaskManager's persistence, retry logic, logging pipeline, and WebSocket notifications.\n async def trigger_run(self, task_id: str, task_manager: TaskManager) -> str:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n if not policy.dashboard_ids:\n raise ValueError(f\"Validation task '{task_id}' has no dashboard IDs configured\")\n if policy.provider_id:\n self._validate_provider(policy.provider_id)\n\n dashboard_id = policy.dashboard_ids[0]\n params: dict[str, Any] = {\n \"dashboard_id\": dashboard_id,\n \"environment_id\": policy.environment_id,\n \"provider_id\": policy.provider_id or \"\",\n \"policy_id\": task_id,\n }\n task = await task_manager.create_task(plugin_id=\"llm_dashboard_validation\", params=params)\n logger.info(\n \"[ValidationTaskService] Triggered run for task '%s' dashboard=%s spawned=%s\",\n policy.name, dashboard_id, task.id,\n )\n return task.id\n\n # #endregion trigger_run\n\n# #endregion ValidationTaskService\n# #endregion ValidationService\n" + "body": "# #region ValidationService [C:3] [TYPE Module] [SEMANTICS validation, service, sqlalchemy, task]\n# @BRIEF Business logic for validation task CRUD and trigger-run.\n# @LAYER Service\n\nfrom datetime import time\nfrom typing import Any\n\nfrom sqlalchemy.orm import Session\n\nfrom ..core.logger import logger\nfrom ..core.task_manager import TaskManager\nfrom ..models.llm import LLMProvider, ValidationPolicy, ValidationRecord\nfrom ..schemas.validation import ValidationTaskCreate, ValidationTaskResponse, ValidationTaskUpdate\nfrom .llm_provider import LLMProviderService\n\n\ndef _parse_time(value: str | None) -> time | None:\n \"\"\"Parse HH:MM string to datetime.time, or return None.\"\"\"\n if not value:\n return None\n try:\n parts = value.strip().split(\":\")\n return time(int(parts[0]), int(parts[1]))\n except (ValueError, IndexError):\n return None\n\n\ndef _format_time(value: time | None) -> str | None:\n \"\"\"Format datetime.time to HH:MM string, or return None.\"\"\"\n if value is None:\n return None\n return f\"{value.hour:02d}:{value.minute:02d}\"\n\n\n# #region ValidationTaskService [C:4] [TYPE Class]\n# @BRIEF Validation task (policy) CRUD with provider validation and trigger-run.\n# @RATIONALE Service-layer class for validation task CRUD with provider validation and trigger-run. Separates API layer concerns from business logic; enables unit testing without HTTP.\n# @REJECTED Embedding CRUD in route handlers rejected — would duplicate validation logic across endpoints; centralized service ensures consistent provider/environment validation before persistence.\nclass ValidationTaskService:\n\n def __init__(self, db: Session, config_manager=None, username: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.username = username or \"system\"\n\n # #region _validate_provider [C:2] [TYPE Function]\n # @RATIONALE Centralizes multimodal LLM provider check so all task operations (create, update, trigger) share the same validation gate. Uses LLMProviderService for reuse.\n # @REJECTED Duplicating multimodal check in each caller rejected — would create maintenance burden if provider validation rules change or error messages need updating.\n def _validate_provider(self, provider_id: str) -> LLMProvider:\n if not provider_id or not provider_id.strip():\n raise ValueError(\"provider_id is required\")\n svc = LLMProviderService(self.db)\n provider = svc.get_provider(provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{provider_id}' not found\")\n if not bool(provider.is_multimodal):\n raise ValueError(\n f\"LLM provider '{provider.name}' is not multimodal. \"\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n return provider\n\n # #endregion _validate_provider\n\n # #region _validate_environment [C:1] [TYPE Function]\n def _validate_environment(self, environment_id: str) -> None:\n if not self.config_manager:\n return\n env = self.config_manager.get_environment(environment_id)\n if not env:\n raise ValueError(f\"Environment '{environment_id}' not found\")\n\n # #endregion _validate_environment\n\n # #region _policy_to_response [C:2] [TYPE Function]\n def _policy_to_response(self, policy: ValidationPolicy) -> ValidationTaskResponse:\n last_run = None\n if policy.dashboard_ids:\n last_run = (\n self.db.query(ValidationRecord)\n .filter(\n ValidationRecord.dashboard_id.in_(policy.dashboard_ids),\n ValidationRecord.environment_id == policy.environment_id,\n )\n .order_by(ValidationRecord.timestamp.desc())\n .first()\n )\n return ValidationTaskResponse(\n id=policy.id,\n name=policy.name,\n environment_id=policy.environment_id,\n is_active=bool(policy.is_active) if policy.is_active is not None else True,\n dashboard_ids=list(policy.dashboard_ids) if policy.dashboard_ids else [],\n provider_id=policy.provider_id,\n schedule_days=list(policy.schedule_days) if policy.schedule_days else None,\n window_start=_format_time(policy.window_start),\n window_end=_format_time(policy.window_end),\n notify_owners=bool(policy.notify_owners) if policy.notify_owners is not None else True,\n custom_channels=list(policy.custom_channels) if policy.custom_channels else None,\n alert_condition=policy.alert_condition or \"FAIL_ONLY\",\n created_at=policy.created_at,\n updated_at=policy.updated_at,\n last_run_status=str(last_run.status) if last_run else None,\n last_run_at=last_run.timestamp if last_run else None,\n )\n\n # #endregion _policy_to_response\n\n # #region list_tasks [C:3] [TYPE Function]\n # @RATIONALE Uses query-time filtering with optional is_active/environment_id params rather than post-filtering results in memory, reducing overhead with large task counts.\n # @REJECTED In-memory filtering after loading all rows rejected — would be inefficient with hundreds of tasks; database-level filtering scales better.\n def list_tasks(\n self,\n is_active: bool | None = None,\n environment_id: str | None = None,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[ValidationTaskResponse]]:\n query = self.db.query(ValidationPolicy)\n if is_active is not None:\n query = query.filter(ValidationPolicy.is_active == is_active)\n if environment_id:\n query = query.filter(ValidationPolicy.environment_id == environment_id)\n\n total = query.count()\n policies = (\n query.order_by(ValidationPolicy.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return total, [self._policy_to_response(p) for p in policies]\n\n # #endregion list_tasks\n\n # #region create_task [C:4] [TYPE Function]\n # @RATIONALE Validates provider (multimodal check) and environment before persisting the policy, ensuring referential integrity at creation time and preventing silent runtime failures.\n # @REJECTED Lazy validation on first run rejected — would allow creation of invalid task configurations with non-multimodal providers that fail only at runtime during dashboard validation.\n def create_task(self, payload: ValidationTaskCreate) -> ValidationTaskResponse:\n self._validate_provider(payload.provider_id)\n self._validate_environment(payload.environment_id)\n\n ws = _parse_time(payload.window_start)\n we = _parse_time(payload.window_end)\n\n policy = ValidationPolicy(\n name=payload.name,\n environment_id=payload.environment_id,\n is_active=payload.is_active,\n dashboard_ids=payload.dashboard_ids or [],\n provider_id=payload.provider_id,\n schedule_days=payload.schedule_days or [],\n window_start=ws or time(9, 0),\n window_end=we or time(18, 0),\n notify_owners=payload.notify_owners,\n custom_channels=payload.custom_channels,\n alert_condition=payload.alert_condition or \"FAIL_ONLY\",\n )\n self.db.add(policy)\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\n \"[ValidationTaskService] Created task '%s' (id=%s)\", policy.name, policy.id\n )\n return self._policy_to_response(policy)\n\n # #endregion create_task\n\n # #region get_task [C:3] [TYPE Function]\n # @RATIONALE Returns full task response including last_run_status via joined subquery — gives the UI immediate visibility into task health without a second API call.\n # @REJECTED Separate query for last run status rejected — double-query pattern would add latency on every task detail request.\n def get_task(self, task_id: str) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n return self._policy_to_response(policy)\n\n # #endregion get_task\n\n # #region update_task [C:4] [TYPE Function]\n # @RATIONALE Re-validates provider/environment only when those fields change (via exclude_unset), avoiding unnecessary DB calls on partial updates and allowing other fields to be updated independently.\n # @REJECTED Full re-validation on every update rejected — would prevent updating non-critical fields (e.g., schedule, name) when a provider is temporarily unavailable.\n def update_task(self, task_id: str, payload: ValidationTaskUpdate) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n update_data = payload.model_dump(exclude_unset=True)\n\n if update_data.get(\"provider_id\"):\n self._validate_provider(update_data[\"provider_id\"])\n if update_data.get(\"environment_id\"):\n self._validate_environment(update_data[\"environment_id\"])\n\n if \"window_start\" in update_data:\n ws = _parse_time(update_data.pop(\"window_start\", None))\n if ws is not None:\n policy.window_start = ws\n if \"window_end\" in update_data:\n we = _parse_time(update_data.pop(\"window_end\", None))\n if we is not None:\n policy.window_end = we\n\n for key, value in update_data.items():\n if hasattr(policy, key):\n setattr(policy, key, value)\n\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\"[ValidationTaskService] Updated task '%s' (id=%s)\", policy.name, policy.id)\n return self._policy_to_response(policy)\n\n # #endregion update_task\n\n # #region delete_task [C:3] [TYPE Function]\n# @RATIONALE Scope run deletion by policy_id instead of dashboard_id+environment_id to prevent deleting runs belonging to other tasks that validate the same dashboard. Requires policy_id column on ValidationRecord, set by the plugin during execution.\n# @REJECTED Always-cascade delete rejected – would destroy run history unexpectedly. Scoping by dashboard_id+environment_id rejected – caused cross-task data loss (F4). Explicit delete_runs flag with policy_id scoping gives users safe control.\n\n def delete_task(self, task_id: str, delete_runs: bool = False) -> None:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n if delete_runs:\n self.db.query(ValidationRecord).filter(\n ValidationRecord.policy_id == policy.id,\n ).delete(synchronize_session=False)\n\n self.db.delete(policy)\n self.db.commit()\n logger.info(\n \"[ValidationTaskService] Deleted task '%s' (id=%s)\", policy.name, policy.id\n )\n\n # #endregion delete_task\n # #region trigger_run [C:4] [TYPE Function]\n # @RATIONALE Uses TaskManager.create_task() to spawn validation via the llm_dashboard_validation plugin — reuses existing scheduling, persistence, retry, and notification infrastructure.\n # @REJECTED Direct LLM provider call from service layer rejected — would bypass TaskManager's persistence, retry logic, logging pipeline, and WebSocket notifications.\n async def trigger_run(self, task_id: str, task_manager: TaskManager) -> str:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n if not policy.dashboard_ids:\n raise ValueError(f\"Validation task '{task_id}' has no dashboard IDs configured\")\n if policy.provider_id:\n self._validate_provider(policy.provider_id)\n\n dashboard_id = policy.dashboard_ids[0]\n params: dict[str, Any] = {\n \"dashboard_id\": dashboard_id,\n \"environment_id\": policy.environment_id,\n \"provider_id\": policy.provider_id or \"\",\n \"policy_id\": task_id,\n }\n task = await task_manager.create_task(plugin_id=\"llm_dashboard_validation\", params=params)\n logger.info(\n \"[ValidationTaskService] Triggered run for task '%s' dashboard=%s spawned=%s\",\n policy.name, dashboard_id, task.id,\n )\n return task.id\n\n # #endregion trigger_run\n\n# #endregion ValidationTaskService\n# #endregion ValidationService\n" }, { "contract_id": "ValidationTaskService", @@ -49154,7 +50034,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region ValidationTaskService [C:4] [TYPE Class]\n# @BRIEF Validation task (policy) CRUD with provider validation and trigger-run.\n# @RATIONALE Service-layer class for validation task CRUD with provider validation and trigger-run. Separates API layer concerns from business logic; enables unit testing without HTTP.\n# @REJECTED Embedding CRUD in route handlers rejected — would duplicate validation logic across endpoints; centralized service ensures consistent provider/environment validation before persistence.\nclass ValidationTaskService:\n\n def __init__(self, db: Session, config_manager=None, username: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.username = username or \"system\"\n\n # #region _validate_provider [C:2] [TYPE Function]\n # @RATIONALE Centralizes multimodal LLM provider check so all task operations (create, update, trigger) share the same validation gate. Uses LLMProviderService for reuse.\n # @REJECTED Duplicating multimodal check in each caller rejected — would create maintenance burden if provider validation rules change or error messages need updating.\n def _validate_provider(self, provider_id: str) -> LLMProvider:\n if not provider_id or not provider_id.strip():\n raise ValueError(\"provider_id is required\")\n svc = LLMProviderService(self.db)\n provider = svc.get_provider(provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{provider_id}' not found\")\n if not bool(provider.is_multimodal):\n raise ValueError(\n f\"LLM provider '{provider.name}' is not multimodal. \"\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n return provider\n\n # #endregion _validate_provider\n\n # #region _validate_environment [C:1] [TYPE Function]\n def _validate_environment(self, environment_id: str) -> None:\n if not self.config_manager:\n return\n env = self.config_manager.get_environment(environment_id)\n if not env:\n raise ValueError(f\"Environment '{environment_id}' not found\")\n\n # #endregion _validate_environment\n\n # #region _policy_to_response [C:2] [TYPE Function]\n def _policy_to_response(self, policy: ValidationPolicy) -> ValidationTaskResponse:\n last_run = None\n if policy.dashboard_ids:\n last_run = (\n self.db.query(ValidationRecord)\n .filter(\n ValidationRecord.dashboard_id.in_(policy.dashboard_ids),\n ValidationRecord.environment_id == policy.environment_id,\n )\n .order_by(ValidationRecord.timestamp.desc())\n .first()\n )\n return ValidationTaskResponse(\n id=policy.id,\n name=policy.name,\n environment_id=policy.environment_id,\n is_active=bool(policy.is_active) if policy.is_active is not None else True,\n dashboard_ids=list(policy.dashboard_ids) if policy.dashboard_ids else [],\n provider_id=policy.provider_id,\n schedule_days=list(policy.schedule_days) if policy.schedule_days else None,\n window_start=_format_time(policy.window_start),\n window_end=_format_time(policy.window_end),\n notify_owners=bool(policy.notify_owners) if policy.notify_owners is not None else True,\n custom_channels=list(policy.custom_channels) if policy.custom_channels else None,\n alert_condition=policy.alert_condition or \"FAIL_ONLY\",\n created_at=policy.created_at,\n updated_at=policy.updated_at,\n last_run_status=str(last_run.status) if last_run else None,\n last_run_at=last_run.timestamp if last_run else None,\n )\n\n # #endregion _policy_to_response\n\n # #region list_tasks [C:3] [TYPE Function]\n # @RATIONALE Uses query-time filtering with optional is_active/environment_id params rather than post-filtering results in memory, reducing overhead with large task counts.\n # @REJECTED In-memory filtering after loading all rows rejected — would be inefficient with hundreds of tasks; database-level filtering scales better.\n def list_tasks(\n self,\n is_active: bool | None = None,\n environment_id: str | None = None,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[ValidationTaskResponse]]:\n query = self.db.query(ValidationPolicy)\n if is_active is not None:\n query = query.filter(ValidationPolicy.is_active == is_active)\n if environment_id:\n query = query.filter(ValidationPolicy.environment_id == environment_id)\n\n total = query.count()\n policies = (\n query.order_by(ValidationPolicy.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return total, [self._policy_to_response(p) for p in policies]\n\n # #endregion list_tasks\n\n # #region create_task [C:4] [TYPE Function]\n # @RATIONALE Validates provider (multimodal check) and environment before persisting the policy, ensuring referential integrity at creation time and preventing silent runtime failures.\n # @REJECTED Lazy validation on first run rejected — would allow creation of invalid task configurations with non-multimodal providers that fail only at runtime during dashboard validation.\n def create_task(self, payload: ValidationTaskCreate) -> ValidationTaskResponse:\n self._validate_provider(payload.provider_id)\n self._validate_environment(payload.environment_id)\n\n ws = _parse_time(payload.window_start)\n we = _parse_time(payload.window_end)\n\n policy = ValidationPolicy(\n name=payload.name,\n environment_id=payload.environment_id,\n is_active=payload.is_active,\n dashboard_ids=payload.dashboard_ids or [],\n provider_id=payload.provider_id,\n schedule_days=payload.schedule_days or [],\n window_start=ws or time(9, 0),\n window_end=we or time(18, 0),\n notify_owners=payload.notify_owners,\n custom_channels=payload.custom_channels,\n alert_condition=payload.alert_condition or \"FAIL_ONLY\",\n )\n self.db.add(policy)\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\n \"[ValidationTaskService] Created task '%s' (id=%s)\", policy.name, policy.id\n )\n return self._policy_to_response(policy)\n\n # #endregion create_task\n\n # #region get_task [C:3] [TYPE Function]\n # @RATIONALE Returns full task response including last_run_status via joined subquery — gives the UI immediate visibility into task health without a second API call.\n # @REJECTED Separate query for last run status rejected — double-query pattern would add latency on every task detail request.\n def get_task(self, task_id: str) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n return self._policy_to_response(policy)\n\n # #endregion get_task\n\n # #region update_task [C:4] [TYPE Function]\n # @RATIONALE Re-validates provider/environment only when those fields change (via exclude_unset), avoiding unnecessary DB calls on partial updates and allowing other fields to be updated independently.\n # @REJECTED Full re-validation on every update rejected — would prevent updating non-critical fields (e.g., schedule, name) when a provider is temporarily unavailable.\n def update_task(self, task_id: str, payload: ValidationTaskUpdate) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n update_data = payload.model_dump(exclude_unset=True)\n\n if \"provider_id\" in update_data and update_data[\"provider_id\"]:\n self._validate_provider(update_data[\"provider_id\"])\n if \"environment_id\" in update_data and update_data[\"environment_id\"]:\n self._validate_environment(update_data[\"environment_id\"])\n\n if \"window_start\" in update_data:\n ws = _parse_time(update_data.pop(\"window_start\", None))\n if ws is not None:\n policy.window_start = ws\n if \"window_end\" in update_data:\n we = _parse_time(update_data.pop(\"window_end\", None))\n if we is not None:\n policy.window_end = we\n\n for key, value in update_data.items():\n if hasattr(policy, key):\n setattr(policy, key, value)\n\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\"[ValidationTaskService] Updated task '%s' (id=%s)\", policy.name, policy.id)\n return self._policy_to_response(policy)\n\n # #endregion update_task\n\n # #region delete_task [C:3] [TYPE Function]\n# @RATIONALE Scope run deletion by policy_id instead of dashboard_id+environment_id to prevent deleting runs belonging to other tasks that validate the same dashboard. Requires policy_id column on ValidationRecord, set by the plugin during execution.\n# @REJECTED Always-cascade delete rejected – would destroy run history unexpectedly. Scoping by dashboard_id+environment_id rejected – caused cross-task data loss (F4). Explicit delete_runs flag with policy_id scoping gives users safe control.\n\n def delete_task(self, task_id: str, delete_runs: bool = False) -> None:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n if delete_runs:\n self.db.query(ValidationRecord).filter(\n ValidationRecord.policy_id == policy.id,\n ).delete(synchronize_session=False)\n\n self.db.delete(policy)\n self.db.commit()\n logger.info(\n \"[ValidationTaskService] Deleted task '%s' (id=%s)\", policy.name, policy.id\n )\n\n # #endregion delete_task\n # #region trigger_run [C:4] [TYPE Function]\n # @RATIONALE Uses TaskManager.create_task() to spawn validation via the llm_dashboard_validation plugin — reuses existing scheduling, persistence, retry, and notification infrastructure.\n # @REJECTED Direct LLM provider call from service layer rejected — would bypass TaskManager's persistence, retry logic, logging pipeline, and WebSocket notifications.\n async def trigger_run(self, task_id: str, task_manager: TaskManager) -> str:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n if not policy.dashboard_ids:\n raise ValueError(f\"Validation task '{task_id}' has no dashboard IDs configured\")\n if policy.provider_id:\n self._validate_provider(policy.provider_id)\n\n dashboard_id = policy.dashboard_ids[0]\n params: dict[str, Any] = {\n \"dashboard_id\": dashboard_id,\n \"environment_id\": policy.environment_id,\n \"provider_id\": policy.provider_id or \"\",\n \"policy_id\": task_id,\n }\n task = await task_manager.create_task(plugin_id=\"llm_dashboard_validation\", params=params)\n logger.info(\n \"[ValidationTaskService] Triggered run for task '%s' dashboard=%s spawned=%s\",\n policy.name, dashboard_id, task.id,\n )\n return task.id\n\n # #endregion trigger_run\n\n# #endregion ValidationTaskService\n" + "body": "# #region ValidationTaskService [C:4] [TYPE Class]\n# @BRIEF Validation task (policy) CRUD with provider validation and trigger-run.\n# @RATIONALE Service-layer class for validation task CRUD with provider validation and trigger-run. Separates API layer concerns from business logic; enables unit testing without HTTP.\n# @REJECTED Embedding CRUD in route handlers rejected — would duplicate validation logic across endpoints; centralized service ensures consistent provider/environment validation before persistence.\nclass ValidationTaskService:\n\n def __init__(self, db: Session, config_manager=None, username: str | None = None):\n self.db = db\n self.config_manager = config_manager\n self.username = username or \"system\"\n\n # #region _validate_provider [C:2] [TYPE Function]\n # @RATIONALE Centralizes multimodal LLM provider check so all task operations (create, update, trigger) share the same validation gate. Uses LLMProviderService for reuse.\n # @REJECTED Duplicating multimodal check in each caller rejected — would create maintenance burden if provider validation rules change or error messages need updating.\n def _validate_provider(self, provider_id: str) -> LLMProvider:\n if not provider_id or not provider_id.strip():\n raise ValueError(\"provider_id is required\")\n svc = LLMProviderService(self.db)\n provider = svc.get_provider(provider_id)\n if not provider:\n raise ValueError(f\"LLM provider '{provider_id}' not found\")\n if not bool(provider.is_multimodal):\n raise ValueError(\n f\"LLM provider '{provider.name}' is not multimodal. \"\n \"Dashboard validation requires a multimodal model (image input support).\"\n )\n return provider\n\n # #endregion _validate_provider\n\n # #region _validate_environment [C:1] [TYPE Function]\n def _validate_environment(self, environment_id: str) -> None:\n if not self.config_manager:\n return\n env = self.config_manager.get_environment(environment_id)\n if not env:\n raise ValueError(f\"Environment '{environment_id}' not found\")\n\n # #endregion _validate_environment\n\n # #region _policy_to_response [C:2] [TYPE Function]\n def _policy_to_response(self, policy: ValidationPolicy) -> ValidationTaskResponse:\n last_run = None\n if policy.dashboard_ids:\n last_run = (\n self.db.query(ValidationRecord)\n .filter(\n ValidationRecord.dashboard_id.in_(policy.dashboard_ids),\n ValidationRecord.environment_id == policy.environment_id,\n )\n .order_by(ValidationRecord.timestamp.desc())\n .first()\n )\n return ValidationTaskResponse(\n id=policy.id,\n name=policy.name,\n environment_id=policy.environment_id,\n is_active=bool(policy.is_active) if policy.is_active is not None else True,\n dashboard_ids=list(policy.dashboard_ids) if policy.dashboard_ids else [],\n provider_id=policy.provider_id,\n schedule_days=list(policy.schedule_days) if policy.schedule_days else None,\n window_start=_format_time(policy.window_start),\n window_end=_format_time(policy.window_end),\n notify_owners=bool(policy.notify_owners) if policy.notify_owners is not None else True,\n custom_channels=list(policy.custom_channels) if policy.custom_channels else None,\n alert_condition=policy.alert_condition or \"FAIL_ONLY\",\n created_at=policy.created_at,\n updated_at=policy.updated_at,\n last_run_status=str(last_run.status) if last_run else None,\n last_run_at=last_run.timestamp if last_run else None,\n )\n\n # #endregion _policy_to_response\n\n # #region list_tasks [C:3] [TYPE Function]\n # @RATIONALE Uses query-time filtering with optional is_active/environment_id params rather than post-filtering results in memory, reducing overhead with large task counts.\n # @REJECTED In-memory filtering after loading all rows rejected — would be inefficient with hundreds of tasks; database-level filtering scales better.\n def list_tasks(\n self,\n is_active: bool | None = None,\n environment_id: str | None = None,\n page: int = 1,\n page_size: int = 20,\n ) -> tuple[int, list[ValidationTaskResponse]]:\n query = self.db.query(ValidationPolicy)\n if is_active is not None:\n query = query.filter(ValidationPolicy.is_active == is_active)\n if environment_id:\n query = query.filter(ValidationPolicy.environment_id == environment_id)\n\n total = query.count()\n policies = (\n query.order_by(ValidationPolicy.created_at.desc())\n .offset((page - 1) * page_size)\n .limit(page_size)\n .all()\n )\n return total, [self._policy_to_response(p) for p in policies]\n\n # #endregion list_tasks\n\n # #region create_task [C:4] [TYPE Function]\n # @RATIONALE Validates provider (multimodal check) and environment before persisting the policy, ensuring referential integrity at creation time and preventing silent runtime failures.\n # @REJECTED Lazy validation on first run rejected — would allow creation of invalid task configurations with non-multimodal providers that fail only at runtime during dashboard validation.\n def create_task(self, payload: ValidationTaskCreate) -> ValidationTaskResponse:\n self._validate_provider(payload.provider_id)\n self._validate_environment(payload.environment_id)\n\n ws = _parse_time(payload.window_start)\n we = _parse_time(payload.window_end)\n\n policy = ValidationPolicy(\n name=payload.name,\n environment_id=payload.environment_id,\n is_active=payload.is_active,\n dashboard_ids=payload.dashboard_ids or [],\n provider_id=payload.provider_id,\n schedule_days=payload.schedule_days or [],\n window_start=ws or time(9, 0),\n window_end=we or time(18, 0),\n notify_owners=payload.notify_owners,\n custom_channels=payload.custom_channels,\n alert_condition=payload.alert_condition or \"FAIL_ONLY\",\n )\n self.db.add(policy)\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\n \"[ValidationTaskService] Created task '%s' (id=%s)\", policy.name, policy.id\n )\n return self._policy_to_response(policy)\n\n # #endregion create_task\n\n # #region get_task [C:3] [TYPE Function]\n # @RATIONALE Returns full task response including last_run_status via joined subquery — gives the UI immediate visibility into task health without a second API call.\n # @REJECTED Separate query for last run status rejected — double-query pattern would add latency on every task detail request.\n def get_task(self, task_id: str) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n return self._policy_to_response(policy)\n\n # #endregion get_task\n\n # #region update_task [C:4] [TYPE Function]\n # @RATIONALE Re-validates provider/environment only when those fields change (via exclude_unset), avoiding unnecessary DB calls on partial updates and allowing other fields to be updated independently.\n # @REJECTED Full re-validation on every update rejected — would prevent updating non-critical fields (e.g., schedule, name) when a provider is temporarily unavailable.\n def update_task(self, task_id: str, payload: ValidationTaskUpdate) -> ValidationTaskResponse:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n update_data = payload.model_dump(exclude_unset=True)\n\n if update_data.get(\"provider_id\"):\n self._validate_provider(update_data[\"provider_id\"])\n if update_data.get(\"environment_id\"):\n self._validate_environment(update_data[\"environment_id\"])\n\n if \"window_start\" in update_data:\n ws = _parse_time(update_data.pop(\"window_start\", None))\n if ws is not None:\n policy.window_start = ws\n if \"window_end\" in update_data:\n we = _parse_time(update_data.pop(\"window_end\", None))\n if we is not None:\n policy.window_end = we\n\n for key, value in update_data.items():\n if hasattr(policy, key):\n setattr(policy, key, value)\n\n self.db.commit()\n self.db.refresh(policy)\n logger.info(\"[ValidationTaskService] Updated task '%s' (id=%s)\", policy.name, policy.id)\n return self._policy_to_response(policy)\n\n # #endregion update_task\n\n # #region delete_task [C:3] [TYPE Function]\n# @RATIONALE Scope run deletion by policy_id instead of dashboard_id+environment_id to prevent deleting runs belonging to other tasks that validate the same dashboard. Requires policy_id column on ValidationRecord, set by the plugin during execution.\n# @REJECTED Always-cascade delete rejected – would destroy run history unexpectedly. Scoping by dashboard_id+environment_id rejected – caused cross-task data loss (F4). Explicit delete_runs flag with policy_id scoping gives users safe control.\n\n def delete_task(self, task_id: str, delete_runs: bool = False) -> None:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n\n if delete_runs:\n self.db.query(ValidationRecord).filter(\n ValidationRecord.policy_id == policy.id,\n ).delete(synchronize_session=False)\n\n self.db.delete(policy)\n self.db.commit()\n logger.info(\n \"[ValidationTaskService] Deleted task '%s' (id=%s)\", policy.name, policy.id\n )\n\n # #endregion delete_task\n # #region trigger_run [C:4] [TYPE Function]\n # @RATIONALE Uses TaskManager.create_task() to spawn validation via the llm_dashboard_validation plugin — reuses existing scheduling, persistence, retry, and notification infrastructure.\n # @REJECTED Direct LLM provider call from service layer rejected — would bypass TaskManager's persistence, retry logic, logging pipeline, and WebSocket notifications.\n async def trigger_run(self, task_id: str, task_manager: TaskManager) -> str:\n policy = self.db.query(ValidationPolicy).filter(ValidationPolicy.id == task_id).first()\n if not policy:\n raise ValueError(f\"Validation task '{task_id}' not found\")\n if not policy.dashboard_ids:\n raise ValueError(f\"Validation task '{task_id}' has no dashboard IDs configured\")\n if policy.provider_id:\n self._validate_provider(policy.provider_id)\n\n dashboard_id = policy.dashboard_ids[0]\n params: dict[str, Any] = {\n \"dashboard_id\": dashboard_id,\n \"environment_id\": policy.environment_id,\n \"provider_id\": policy.provider_id or \"\",\n \"policy_id\": task_id,\n }\n task = await task_manager.create_task(plugin_id=\"llm_dashboard_validation\", params=params)\n logger.info(\n \"[ValidationTaskService] Triggered run for task '%s' dashboard=%s spawned=%s\",\n policy.name, dashboard_id, task.id,\n )\n return task.id\n\n # #endregion trigger_run\n\n# #endregion ValidationTaskService\n" }, { "contract_id": "_validate_provider", @@ -49214,7 +50094,7 @@ "contract_type": "Module", "file_path": "backend/tests/conftest.py", "start_line": 1, - "end_line": 93, + "end_line": 94, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49234,7 +50114,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestSessionConfig [C:2] [TYPE Module] [SEMANTICS test, conftest, db, postgres, testcontainers]\n# @BRIEF Shared pytest fixtures and session configuration for backend tests.\n# @RELATION BINDS_TO -> [init_db]\n# @PRE All test modules use sys.path patching to import from src/.\n# @POST Database tables exist before test session executes.\n# @RATIONALE\n# Architecture:\n# - Global DATABASE_URL → temp-file SQLite (for modules importing src.core.database)\n# - Per-module engines → sqlite:///:memory: (for modules with local create_engine)\n# - FK enforcement on ALL engines via PRAGMA foreign_keys=ON\n#\n# Why:\n# - In-process SQLite is 5-10x faster than Docker PostgreSQL\n# - FK enforcement catches constraint violations (unlike plain SQLite)\n# - Per-module engines prevent 10GB+ shared-cache memory leak\n# - Temp file for global engine also prevents memory leak\n#\n# PostgreSQL mode (TEST_DB=postgres) → testcontainers or explicit DATABASE_URL.\n#\n# @REJECTED\n# sqlite:///file::memory:...?cache=shared — 10GB+ memory leak (all modules share one DB).\n# Plain SQLite without FK — silently ignores FK violations, masks bugs.\n# Always-on PostgresContainer — 5-10x slower, Docker dependency.\nimport os\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom sqlalchemy import create_engine, event\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\n\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\n\n# ── Global engine: temp-file SQLite (not in-memory, not shared cache) ──\n# This prevents the 10GB memory leak from shared in-memory databases.\n\n_TEST_DB_FILE = tempfile.NamedTemporaryFile(suffix=\"_ss_tools.db\", delete=False)\n_TEST_DB_PATH = _TEST_DB_FILE.name\n_TEST_DB_FILE.close()\n\nos.environ.setdefault(\"DATABASE_URL\", f\"sqlite:///{_TEST_DB_PATH}\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", f\"sqlite:///{_TEST_DB_PATH}\")\n\n# ── FK enforcement on the global engine ──\n\nfrom src.core.database import engine\n\nevent.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n\n\n# ── Helper: FK-enabled SQLite engine for per-module isolation ──\n\ndef make_fk_engine():\n \"\"\"Create a per-module SQLite in-memory engine with FK enforcement.\n\n Each test module calls this once at module level to get its own\n isolated database. No shared state → no memory leak.\n \"\"\"\n eng = create_engine(\"sqlite:///:memory:\", connect_args={\"check_same_thread\": False})\n event.listen(eng, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n return eng\n\n\n# ── Test session lifecycle ──\n\ndef pytest_configure(config):\n print(\n f\"\\n[conftest] SQLite (global: {_TEST_DB_PATH}) + FK enforcement\",\n file=sys.stderr,\n )\n\n\ndef pytest_unconfigure(config):\n try:\n os.unlink(_TEST_DB_PATH)\n except Exception:\n pass\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef ensure_db_tables():\n \"\"\"Create all tables on the global database.\"\"\"\n try:\n from src.core.database import init_db\n init_db()\n except Exception as exc:\n print(f\"\\n[conftest] init_db failed: {exc}\", file=sys.stderr)\n raise\n\n\n# #endregion TestSessionConfig\n" + "body": "# #region TestSessionConfig [C:2] [TYPE Module] [SEMANTICS test, conftest, db, postgres, testcontainers]\n# @BRIEF Shared pytest fixtures and session configuration for backend tests.\n# @RELATION BINDS_TO -> [init_db]\n# @PRE All test modules use sys.path patching to import from src/.\n# @POST Database tables exist before test session executes.\n# @RATIONALE\n# Architecture:\n# - Global DATABASE_URL → temp-file SQLite (for modules importing src.core.database)\n# - Per-module engines → sqlite:///:memory: (for modules with local create_engine)\n# - FK enforcement on ALL engines via PRAGMA foreign_keys=ON\n#\n# Why:\n# - In-process SQLite is 5-10x faster than Docker PostgreSQL\n# - FK enforcement catches constraint violations (unlike plain SQLite)\n# - Per-module engines prevent 10GB+ shared-cache memory leak\n# - Temp file for global engine also prevents memory leak\n#\n# PostgreSQL mode (TEST_DB=postgres) → testcontainers or explicit DATABASE_URL.\n#\n# @REJECTED\n# sqlite:///file::memory:...?cache=shared — 10GB+ memory leak (all modules share one DB).\n# Plain SQLite without FK — silently ignores FK violations, masks bugs.\n# Always-on PostgresContainer — 5-10x slower, Docker dependency.\nimport os\nfrom pathlib import Path\nimport sys\nimport tempfile\n\nfrom sqlalchemy import create_engine, event\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\n\nos.environ.setdefault(\"SECRET_KEY\", \"test-secret-key-for-testing\")\n\n# ── Global engine: temp-file SQLite (not in-memory, not shared cache) ──\n# This prevents the 10GB memory leak from shared in-memory databases.\n\n_TEST_DB_FILE = tempfile.NamedTemporaryFile(suffix=\"_ss_tools.db\", delete=False)\n_TEST_DB_PATH = _TEST_DB_FILE.name\n_TEST_DB_FILE.close()\n\nos.environ.setdefault(\"DATABASE_URL\", f\"sqlite:///{_TEST_DB_PATH}\")\nos.environ.setdefault(\"AUTH_DATABASE_URL\", f\"sqlite:///{_TEST_DB_PATH}\")\n\n# ── FK enforcement on the global engine ──\n\nfrom src.core.database import engine\n\nevent.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n\n\n# ── Helper: FK-enabled SQLite engine for per-module isolation ──\n\ndef make_fk_engine():\n \"\"\"Create a per-module SQLite in-memory engine with FK enforcement.\n\n Each test module calls this once at module level to get its own\n isolated database. No shared state → no memory leak.\n \"\"\"\n eng = create_engine(\"sqlite:///:memory:\", connect_args={\"check_same_thread\": False})\n event.listen(eng, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n return eng\n\n\n# ── Test session lifecycle ──\n\ndef pytest_configure(config):\n print(\n f\"\\n[conftest] SQLite (global: {_TEST_DB_PATH}) + FK enforcement\",\n file=sys.stderr,\n )\n\n\ndef pytest_unconfigure(config):\n try:\n os.unlink(_TEST_DB_PATH)\n except Exception:\n pass\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef ensure_db_tables():\n \"\"\"Create all tables on the global database.\"\"\"\n try:\n from src.core.database import init_db\n init_db()\n except Exception as exc:\n print(f\"\\n[conftest] init_db failed: {exc}\", file=sys.stderr)\n raise\n\n\n# #endregion TestSessionConfig\n" }, { "contract_id": "TestArchiveParser", @@ -49260,7 +50140,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestArchiveParser [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MigrationArchiveParserModule]\n#\nimport os\nimport sys\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport yaml\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\nfrom src.core.migration.archive_parser import MigrationArchiveParser\n\n\n# #region test_extract_objects_from_zip_collects_all_types [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestArchiveParser\n# @PURPOSE: Verify archive parser collects dashboard/chart/dataset YAML objects into typed buckets.\n# @TEST_CONTRACT: zip_archive_fixture -> typed dashboard/chart/dataset extraction buckets\n# @TEST_SCENARIO: archive_with_supported_objects_extracts_all_types -> One YAML file per supported type lands in matching bucket.\n# @TEST_EDGE: missing_field -> Missing typed folder prevents that object class from being extracted.\n# @TEST_EDGE: invalid_type -> Unsupported file layout is ignored by typed extraction buckets.\n# @TEST_EDGE: external_fail -> Corrupt archive would fail extraction before typed buckets are returned.\ndef test_extract_objects_from_zip_collects_all_types():\n parser = MigrationArchiveParser()\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"objects.zip\"\n src_dir = td_path / \"src\"\n (src_dir / \"dashboards\").mkdir(parents=True)\n (src_dir / \"charts\").mkdir(parents=True)\n (src_dir / \"datasets\").mkdir(parents=True)\n\n with open(src_dir / \"dashboards\" / \"dash.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"dash-u1\", \"dashboard_title\": \"D1\", \"json_metadata\": \"{}\"},\n file_obj,\n )\n with open(src_dir / \"charts\" / \"chart.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"chart-u1\", \"slice_name\": \"C1\", \"viz_type\": \"bar\"}, file_obj\n )\n with open(src_dir / \"datasets\" / \"dataset.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"ds-u1\", \"table_name\": \"orders\", \"database_uuid\": \"db-u1\"},\n file_obj,\n )\n\n with zipfile.ZipFile(zip_path, \"w\") as zip_obj:\n for root, _, files in os.walk(src_dir):\n for file_name in files:\n file_path = Path(root) / file_name\n zip_obj.write(file_path, file_path.relative_to(src_dir))\n\n extracted = parser.extract_objects_from_zip(str(zip_path))\n\n if len(extracted[\"dashboards\"]) != 1:\n raise AssertionError(\"dashboards extraction size mismatch\")\n if extracted[\"dashboards\"][0][\"uuid\"] != \"dash-u1\":\n raise AssertionError(\"dashboard uuid mismatch\")\n if len(extracted[\"charts\"]) != 1:\n raise AssertionError(\"charts extraction size mismatch\")\n if extracted[\"charts\"][0][\"uuid\"] != \"chart-u1\":\n raise AssertionError(\"chart uuid mismatch\")\n if len(extracted[\"datasets\"]) != 1:\n raise AssertionError(\"datasets extraction size mismatch\")\n if extracted[\"datasets\"][0][\"uuid\"] != \"ds-u1\":\n raise AssertionError(\"dataset uuid mismatch\")\n\n\n# #endregion test_extract_objects_from_zip_collects_all_types\n# #endregion TestArchiveParser\n" + "body": "# #region TestArchiveParser [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MigrationArchiveParserModule]\n#\nimport os\nfrom pathlib import Path\nimport sys\nimport tempfile\nimport zipfile\n\nimport yaml\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\nfrom src.core.migration.archive_parser import MigrationArchiveParser\n\n\n# #region test_extract_objects_from_zip_collects_all_types [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestArchiveParser\n# @PURPOSE: Verify archive parser collects dashboard/chart/dataset YAML objects into typed buckets.\n# @TEST_CONTRACT: zip_archive_fixture -> typed dashboard/chart/dataset extraction buckets\n# @TEST_SCENARIO: archive_with_supported_objects_extracts_all_types -> One YAML file per supported type lands in matching bucket.\n# @TEST_EDGE: missing_field -> Missing typed folder prevents that object class from being extracted.\n# @TEST_EDGE: invalid_type -> Unsupported file layout is ignored by typed extraction buckets.\n# @TEST_EDGE: external_fail -> Corrupt archive would fail extraction before typed buckets are returned.\ndef test_extract_objects_from_zip_collects_all_types():\n parser = MigrationArchiveParser()\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"objects.zip\"\n src_dir = td_path / \"src\"\n (src_dir / \"dashboards\").mkdir(parents=True)\n (src_dir / \"charts\").mkdir(parents=True)\n (src_dir / \"datasets\").mkdir(parents=True)\n\n with open(src_dir / \"dashboards\" / \"dash.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"dash-u1\", \"dashboard_title\": \"D1\", \"json_metadata\": \"{}\"},\n file_obj,\n )\n with open(src_dir / \"charts\" / \"chart.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"chart-u1\", \"slice_name\": \"C1\", \"viz_type\": \"bar\"}, file_obj\n )\n with open(src_dir / \"datasets\" / \"dataset.yaml\", \"w\") as file_obj:\n yaml.dump(\n {\"uuid\": \"ds-u1\", \"table_name\": \"orders\", \"database_uuid\": \"db-u1\"},\n file_obj,\n )\n\n with zipfile.ZipFile(zip_path, \"w\") as zip_obj:\n for root, _, files in os.walk(src_dir):\n for file_name in files:\n file_path = Path(root) / file_name\n zip_obj.write(file_path, file_path.relative_to(src_dir))\n\n extracted = parser.extract_objects_from_zip(str(zip_path))\n\n if len(extracted[\"dashboards\"]) != 1:\n raise AssertionError(\"dashboards extraction size mismatch\")\n if extracted[\"dashboards\"][0][\"uuid\"] != \"dash-u1\":\n raise AssertionError(\"dashboard uuid mismatch\")\n if len(extracted[\"charts\"]) != 1:\n raise AssertionError(\"charts extraction size mismatch\")\n if extracted[\"charts\"][0][\"uuid\"] != \"chart-u1\":\n raise AssertionError(\"chart uuid mismatch\")\n if len(extracted[\"datasets\"]) != 1:\n raise AssertionError(\"datasets extraction size mismatch\")\n if extracted[\"datasets\"][0][\"uuid\"] != \"ds-u1\":\n raise AssertionError(\"dataset uuid mismatch\")\n\n\n# #endregion test_extract_objects_from_zip_collects_all_types\n# #endregion TestArchiveParser\n" }, { "contract_id": "test_extract_objects_from_zip_collects_all_types", @@ -49314,7 +50194,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDryRunOrchestrator [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MigrationDryRunOrchestratorModule]\n#\nimport json\nimport sys\nfrom pathlib import Path\nfrom unittest.mock import MagicMock, patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\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\nfrom src.core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom src.models.dashboard import DashboardSelection\nfrom src.models.mapping import Base\n\n\n# #region _load_fixture [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Load canonical migration dry-run fixture payload used by deterministic orchestration assertions.\ndef _load_fixture() -> dict:\n fixture_path = (\n Path(__file__).parents[2] / \"fixtures\" / \"migration_dry_run_fixture.json\"\n )\n return json.loads(fixture_path.read_text())\n\n\n# #endregion _load_fixture\n\n\n# #region _make_session [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Build isolated in-memory SQLAlchemy session for dry-run service tests.\ndef _make_session():\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 return Session()\n\n\n# #endregion _make_session\n\n\n# #region test_migration_dry_run_service_builds_diff_and_risk [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Verify dry-run orchestration returns stable diff summary and required risk codes.\n# @TEST_SCENARIO: dry_run_builds_diff_and_risk -> Stable diff summary and required risk codes are returned.\n# @TEST_EDGE: missing_field -> Missing target datasource remains visible in risk items.\n# @TEST_EDGE: invalid_type -> Broken dataset reference remains visible in risk items.\n# @TEST_EDGE: external_fail -> Engine transform stub failure would stop result production.\n# @TEST_INVARIANT: dry_run_result_contract_matches_fixture -> VERIFIED_BY: [dry_run_builds_diff_and_risk]\ndef test_migration_dry_run_service_builds_diff_and_risk():\n # @TEST_CONTRACT: dry_run_result_contract -> {\n # required_fields: {diff: object, summary: object, risk: object},\n # invariants: [\"risk.score >= 0\", \"summary.selected_dashboards == len(selection.selected_ids)\"]\n # }\n # @TEST_FIXTURE: migration_dry_run_fixture -> backend/tests/fixtures/migration_dry_run_fixture.json\n # @TEST_EDGE: missing_target_datasource -> fixture.transformed_zip_objects.datasets[0].database_uuid\n # @TEST_EDGE: breaking_reference -> fixture.transformed_zip_objects.charts[0].dataset_uuid\n fixture = _load_fixture()\n db = _make_session()\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 source_client = MagicMock()\n source_client.get_dashboards_summary.return_value = fixture[\n \"source_dashboard_summary\"\n ]\n source_client.export_dashboard.return_value = (b\"PK\\x03\\x04\", \"source.zip\")\n\n target_client = MagicMock()\n target_client.get_dashboards.return_value = (\n len(fixture[\"target\"][\"dashboards\"]),\n fixture[\"target\"][\"dashboards\"],\n )\n target_client.get_datasets.return_value = (\n len(fixture[\"target\"][\"datasets\"]),\n fixture[\"target\"][\"datasets\"],\n )\n target_client.get_charts.return_value = (\n len(fixture[\"target\"][\"charts\"]),\n fixture[\"target\"][\"charts\"],\n )\n target_client.get_databases.return_value = (\n len(fixture[\"target\"][\"databases\"]),\n fixture[\"target\"][\"databases\"],\n )\n\n parser = MagicMock()\n parser.extract_objects_from_zip.return_value = fixture[\"transformed_zip_objects\"]\n service = MigrationDryRunService(parser=parser)\n\n with patch(\"src.core.migration.dry_run_orchestrator.MigrationEngine\") as EngineMock:\n engine = MagicMock()\n engine.transform_zip.return_value = True\n EngineMock.return_value = engine\n result = service.run(selection, source_client, target_client, db)\n\n if \"summary\" not in result:\n raise AssertionError(\"summary is missing in dry-run payload\")\n if result[\"summary\"][\"selected_dashboards\"] != 1:\n raise AssertionError(\"selected_dashboards summary mismatch\")\n if result[\"summary\"][\"dashboards\"][\"update\"] != 1:\n raise AssertionError(\"dashboard update count mismatch\")\n if result[\"summary\"][\"charts\"][\"create\"] != 1:\n raise AssertionError(\"chart create count mismatch\")\n if result[\"summary\"][\"datasets\"][\"create\"] != 1:\n raise AssertionError(\"dataset create count mismatch\")\n\n risk_codes = {item[\"code\"] for item in result[\"risk\"][\"items\"]}\n if \"missing_datasource\" not in risk_codes:\n raise AssertionError(\"missing_datasource risk is not detected\")\n if \"breaking_reference\" not in risk_codes:\n raise AssertionError(\"breaking_reference risk is not detected\")\n\n\n# #endregion test_migration_dry_run_service_builds_diff_and_risk\n# #endregion TestDryRunOrchestrator\n" + "body": "# #region TestDryRunOrchestrator [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.\n# @LAYER Domain\n# @RELATION DEPENDS_ON -> [MigrationDryRunOrchestratorModule]\n#\nimport json\nfrom pathlib import Path\nimport sys\nfrom unittest.mock import MagicMock, patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.pool import StaticPool\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\nfrom src.core.migration.dry_run_orchestrator import MigrationDryRunService\nfrom src.models.dashboard import DashboardSelection\nfrom src.models.mapping import Base\n\n\n# #region _load_fixture [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Load canonical migration dry-run fixture payload used by deterministic orchestration assertions.\ndef _load_fixture() -> dict:\n fixture_path = (\n Path(__file__).parents[2] / \"fixtures\" / \"migration_dry_run_fixture.json\"\n )\n return json.loads(fixture_path.read_text())\n\n\n# #endregion _load_fixture\n\n\n# #region _make_session [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Build isolated in-memory SQLAlchemy session for dry-run service tests.\ndef _make_session():\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 return Session()\n\n\n# #endregion _make_session\n\n\n# #region test_migration_dry_run_service_builds_diff_and_risk [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestDryRunOrchestrator]\n# @PURPOSE: Verify dry-run orchestration returns stable diff summary and required risk codes.\n# @TEST_SCENARIO: dry_run_builds_diff_and_risk -> Stable diff summary and required risk codes are returned.\n# @TEST_EDGE: missing_field -> Missing target datasource remains visible in risk items.\n# @TEST_EDGE: invalid_type -> Broken dataset reference remains visible in risk items.\n# @TEST_EDGE: external_fail -> Engine transform stub failure would stop result production.\n# @TEST_INVARIANT: dry_run_result_contract_matches_fixture -> VERIFIED_BY: [dry_run_builds_diff_and_risk]\ndef test_migration_dry_run_service_builds_diff_and_risk():\n # @TEST_CONTRACT: dry_run_result_contract -> {\n # required_fields: {diff: object, summary: object, risk: object},\n # invariants: [\"risk.score >= 0\", \"summary.selected_dashboards == len(selection.selected_ids)\"]\n # }\n # @TEST_FIXTURE: migration_dry_run_fixture -> backend/tests/fixtures/migration_dry_run_fixture.json\n # @TEST_EDGE: missing_target_datasource -> fixture.transformed_zip_objects.datasets[0].database_uuid\n # @TEST_EDGE: breaking_reference -> fixture.transformed_zip_objects.charts[0].dataset_uuid\n fixture = _load_fixture()\n db = _make_session()\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 source_client = MagicMock()\n source_client.get_dashboards_summary.return_value = fixture[\n \"source_dashboard_summary\"\n ]\n source_client.export_dashboard.return_value = (b\"PK\\x03\\x04\", \"source.zip\")\n\n target_client = MagicMock()\n target_client.get_dashboards.return_value = (\n len(fixture[\"target\"][\"dashboards\"]),\n fixture[\"target\"][\"dashboards\"],\n )\n target_client.get_datasets.return_value = (\n len(fixture[\"target\"][\"datasets\"]),\n fixture[\"target\"][\"datasets\"],\n )\n target_client.get_charts.return_value = (\n len(fixture[\"target\"][\"charts\"]),\n fixture[\"target\"][\"charts\"],\n )\n target_client.get_databases.return_value = (\n len(fixture[\"target\"][\"databases\"]),\n fixture[\"target\"][\"databases\"],\n )\n\n parser = MagicMock()\n parser.extract_objects_from_zip.return_value = fixture[\"transformed_zip_objects\"]\n service = MigrationDryRunService(parser=parser)\n\n with patch(\"src.core.migration.dry_run_orchestrator.MigrationEngine\") as EngineMock:\n engine = MagicMock()\n engine.transform_zip.return_value = True\n EngineMock.return_value = engine\n result = service.run(selection, source_client, target_client, db)\n\n if \"summary\" not in result:\n raise AssertionError(\"summary is missing in dry-run payload\")\n if result[\"summary\"][\"selected_dashboards\"] != 1:\n raise AssertionError(\"selected_dashboards summary mismatch\")\n if result[\"summary\"][\"dashboards\"][\"update\"] != 1:\n raise AssertionError(\"dashboard update count mismatch\")\n if result[\"summary\"][\"charts\"][\"create\"] != 1:\n raise AssertionError(\"chart create count mismatch\")\n if result[\"summary\"][\"datasets\"][\"create\"] != 1:\n raise AssertionError(\"dataset create count mismatch\")\n\n risk_codes = {item[\"code\"] for item in result[\"risk\"][\"items\"]}\n if \"missing_datasource\" not in risk_codes:\n raise AssertionError(\"missing_datasource risk is not detected\")\n if \"breaking_reference\" not in risk_codes:\n raise AssertionError(\"breaking_reference risk is not detected\")\n\n\n# #endregion test_migration_dry_run_service_builds_diff_and_risk\n# #endregion TestDryRunOrchestrator\n" }, { "contract_id": "_load_fixture", @@ -49374,8 +50254,8 @@ "contract_id": "test_git_service_get_repo_path_guard", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 16, - "end_line": 24, + "start_line": 14, + "end_line": 22, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49391,8 +50271,8 @@ "contract_id": "test_git_service_get_repo_path_recreates_base_dir", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 26, - "end_line": 37, + "start_line": 24, + "end_line": 35, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49408,8 +50288,8 @@ "contract_id": "test_superset_client_import_dashboard_guard", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 39, - "end_line": 54, + "start_line": 37, + "end_line": 52, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49425,8 +50305,8 @@ "contract_id": "test_git_service_init_repo_reclones_when_path_is_not_a_git_repo", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 56, - "end_line": 75, + "start_line": 54, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49442,8 +50322,8 @@ "contract_id": "test_git_service_ensure_gitflow_branches_creates_and_pushes_missing_defaults", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 77, - "end_line": 132, + "start_line": 75, + "end_line": 130, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49459,8 +50339,8 @@ "contract_id": "test_git_service_configure_identity_updates_repo_local_config", "contract_type": "Function", "file_path": "backend/tests/core/test_defensive_guards.py", - "start_line": 134, - "end_line": 150, + "start_line": 132, + "end_line": 148, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -49504,7 +50384,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestGitServiceGiteaPr [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, git, gitea, pull_request, fallback\n# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.\n# @LAYER Domain\n# @INVARIANT: A 404 from primary Gitea URL retries once against remote-url host when different.\n\nimport asyncio\nimport sys\nfrom pathlib import Path\n\nimport pytest\nfrom fastapi import HTTPException\n\nsys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\nfrom src.services.git_service import GitService\n\n\n# #region test_derive_server_url_from_remote_strips_credentials [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure helper returns host base URL and removes embedded credentials.\n# @PRE: remote_url is an https URL with username/token.\n# @POST: Result is scheme+host only.\ndef test_derive_server_url_from_remote_strips_credentials():\n service = GitService(base_path=\"test_repos\")\n derived = service._derive_server_url_from_remote(\n \"https://oauth2:token@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\"\n )\n assert derived == \"https://giteabusya.bebesh.ru\"\n# #endregion test_derive_server_url_from_remote_strips_credentials\n\n\n# #region test_create_gitea_pull_request_retries_with_remote_host_on_404 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Verify create_gitea_pull_request retries with remote URL host after primary 404.\n# @PRE: primary server_url differs from remote_url host.\n# @POST: Method returns success payload from fallback request.\ndef test_create_gitea_pull_request_retries_with_remote_host_on_404(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n calls = []\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n calls.append((method, server_url, endpoint))\n if len(calls) == 1:\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n return {\"number\": 42, \"html_url\": \"https://giteabusya.bebesh.ru/busya/covid-vaccine-dashboard/pulls/42\", \"state\": \"open\"}\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n result = asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://oauth2:secret@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"ss-dev\",\n to_branch=\"main\",\n title=\"Promote ss-dev -> main\",\n description=\"\",\n )\n )\n\n assert result[\"id\"] == 42\n assert len(calls) == 2\n assert calls[0][1] == \"https://gitea.bebesh.ru\"\n assert calls[1][1] == \"https://giteabusya.bebesh.ru\"\n# #endregion test_create_gitea_pull_request_retries_with_remote_host_on_404\n\n\n# #region test_create_gitea_pull_request_returns_branch_error_when_target_missing [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure Gitea 404 on PR creation is mapped to actionable target-branch validation error.\n# @PRE: PR create call returns 404 and target branch is absent.\n# @POST: Service raises HTTPException 400 with explicit missing target branch message.\ndef test_create_gitea_pull_request_returns_branch_error_when_target_missing(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n if method == \"POST\" and endpoint.endswith(\"/pulls\"):\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n if method == \"GET\" and endpoint.endswith(\"/branches/dev\"):\n return {\"name\": \"dev\"}\n if method == \"GET\" and endpoint.endswith(\"/branches/preprod\"):\n raise HTTPException(status_code=404, detail=\"branch not found\")\n raise AssertionError(f\"Unexpected request: {method} {endpoint}\")\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://gitea.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"dev\",\n to_branch=\"preprod\",\n title=\"Promote dev -> preprod\",\n description=\"\",\n )\n )\n\n assert exc_info.value.status_code == 400\n assert \"target branch 'preprod'\" in str(exc_info.value.detail)\n# #endregion test_create_gitea_pull_request_returns_branch_error_when_target_missing\n\n# #endregion TestGitServiceGiteaPr\n" + "body": "# #region TestGitServiceGiteaPr [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, git, gitea, pull_request, fallback\n# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.\n# @LAYER Domain\n# @INVARIANT: A 404 from primary Gitea URL retries once against remote-url host when different.\n\nimport asyncio\nfrom pathlib import Path\nimport pytest\nimport sys\n\nfrom fastapi import HTTPException\n\nsys.path.insert(0, str(Path(__file__).parent.parent.parent))\n\nfrom src.services.git_service import GitService\n\n\n# #region test_derive_server_url_from_remote_strips_credentials [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure helper returns host base URL and removes embedded credentials.\n# @PRE: remote_url is an https URL with username/token.\n# @POST: Result is scheme+host only.\ndef test_derive_server_url_from_remote_strips_credentials():\n service = GitService(base_path=\"test_repos\")\n derived = service._derive_server_url_from_remote(\n \"https://oauth2:token@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\"\n )\n assert derived == \"https://giteabusya.bebesh.ru\"\n# #endregion test_derive_server_url_from_remote_strips_credentials\n\n\n# #region test_create_gitea_pull_request_retries_with_remote_host_on_404 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Verify create_gitea_pull_request retries with remote URL host after primary 404.\n# @PRE: primary server_url differs from remote_url host.\n# @POST: Method returns success payload from fallback request.\ndef test_create_gitea_pull_request_retries_with_remote_host_on_404(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n calls = []\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n calls.append((method, server_url, endpoint))\n if len(calls) == 1:\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n return {\"number\": 42, \"html_url\": \"https://giteabusya.bebesh.ru/busya/covid-vaccine-dashboard/pulls/42\", \"state\": \"open\"}\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n result = asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://oauth2:secret@giteabusya.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"ss-dev\",\n to_branch=\"main\",\n title=\"Promote ss-dev -> main\",\n description=\"\",\n )\n )\n\n assert result[\"id\"] == 42\n assert len(calls) == 2\n assert calls[0][1] == \"https://gitea.bebesh.ru\"\n assert calls[1][1] == \"https://giteabusya.bebesh.ru\"\n# #endregion test_create_gitea_pull_request_retries_with_remote_host_on_404\n\n\n# #region test_create_gitea_pull_request_returns_branch_error_when_target_missing [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestGitServiceGiteaPr\n# @PURPOSE: Ensure Gitea 404 on PR creation is mapped to actionable target-branch validation error.\n# @PRE: PR create call returns 404 and target branch is absent.\n# @POST: Service raises HTTPException 400 with explicit missing target branch message.\ndef test_create_gitea_pull_request_returns_branch_error_when_target_missing(monkeypatch):\n service = GitService(base_path=\"test_repos\")\n\n async def fake_gitea_request(method, server_url, pat, endpoint, payload=None):\n if method == \"POST\" and endpoint.endswith(\"/pulls\"):\n raise HTTPException(status_code=404, detail=\"Gitea API error: The target couldn't be found.\")\n if method == \"GET\" and endpoint.endswith(\"/branches/dev\"):\n return {\"name\": \"dev\"}\n if method == \"GET\" and endpoint.endswith(\"/branches/preprod\"):\n raise HTTPException(status_code=404, detail=\"branch not found\")\n raise AssertionError(f\"Unexpected request: {method} {endpoint}\")\n\n monkeypatch.setattr(service, \"_gitea_request\", fake_gitea_request)\n\n with pytest.raises(HTTPException) as exc_info:\n asyncio.run(\n service.create_gitea_pull_request(\n server_url=\"https://gitea.bebesh.ru\",\n pat=\"secret\",\n remote_url=\"https://gitea.bebesh.ru/busya/covid-vaccine-dashboard.git\",\n from_branch=\"dev\",\n to_branch=\"preprod\",\n title=\"Promote dev -> preprod\",\n description=\"\",\n )\n )\n\n assert exc_info.value.status_code == 400\n assert \"target branch 'preprod'\" in str(exc_info.value.detail)\n# #endregion test_create_gitea_pull_request_returns_branch_error_when_target_missing\n\n# #endregion TestGitServiceGiteaPr\n" }, { "contract_id": "test_derive_server_url_from_remote_strips_credentials", @@ -49611,7 +50491,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestMappingService [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.\n# @LAYER Domain\n# @RELATION BINDS_TO ->[IdMappingService]\n#\nimport sys\nfrom datetime import UTC, datetime\nfrom pathlib import Path\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\n\n# Add backend directory to sys.path so 'src' can be resolved\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import Base, Environment, ResourceMapping, ResourceType\n\n\n@pytest.fixture\ndef db_session():\n # In-memory SQLite for testing\n engine = create_engine(\"sqlite:///:memory:\")\n event.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n # Create FK parent Environment records for all environment_ids used in tests\n for env_id in ['test-env', 'prod-env-1', 'env1']:\n if not session.query(Environment).filter(Environment.id == env_id).first():\n session.add(Environment(\n id=env_id, name=env_id,\n url=f'http://{env_id}.example.com',\n credentials_id=env_id\n ))\n session.commit()\n yield session\n session.close()\n\n\nclass MockSupersetClient:\n def __init__(self, resources):\n self.resources = resources\n\n def get_all_resources(self, endpoint, since_dttm=None):\n return self.resources.get(endpoint, [])\n\n\n# #region test_sync_environment_upserts_correctly [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_upserts_correctly(db_session):\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Test Chart\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = db_session.query(ResourceMapping).first()\n assert mapping is not None\n assert mapping.environment_id == \"test-env\"\n assert mapping.resource_type == ResourceType.CHART\n assert mapping.uuid == \"123e4567-e89b-12d3-a456-426614174000\"\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Test Chart\"\n\n\n# #endregion test_sync_environment_upserts_correctly\n\n\n# #region test_get_remote_id_returns_integer [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(UTC),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# #endregion test_get_remote_id_returns_integer\n\n\n# #region test_get_remote_ids_batch_returns_dict [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_dict(db_session):\n service = IdMappingService(db_session)\n m1 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-1\",\n remote_integer_id=\"11\",\n )\n m2 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-2\",\n remote_integer_id=\"22\",\n )\n db_session.add_all([m1, m2])\n db_session.commit()\n\n result = service.get_remote_ids_batch(\n \"test-env\", ResourceType.DASHBOARD, [\"uuid-1\", \"uuid-2\", \"uuid-missing\"]\n )\n\n assert len(result) == 2\n assert result[\"uuid-1\"] == 11\n assert result[\"uuid-2\"] == 22\n assert \"uuid-missing\" not in result\n\n\n# #endregion test_get_remote_ids_batch_returns_dict\n\n\n# #region test_sync_environment_updates_existing_mapping [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_updates_existing_mapping(db_session):\n \"\"\"Verify that sync_environment updates an existing mapping (upsert UPDATE path).\"\"\"\n from src.models.mapping import ResourceMapping\n\n # Pre-populate a mapping\n existing = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.CHART,\n uuid=\"123e4567-e89b-12d3-a456-426614174000\",\n remote_integer_id=\"10\",\n resource_name=\"Old Name\",\n )\n db_session.add(existing)\n db_session.commit()\n\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Updated Name\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = (\n db_session.query(ResourceMapping)\n .filter_by(uuid=\"123e4567-e89b-12d3-a456-426614174000\")\n .first()\n )\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Updated Name\"\n # Should still be only one record (updated, not duplicated)\n count = db_session.query(ResourceMapping).count()\n assert count == 1\n\n\n# #endregion test_sync_environment_updates_existing_mapping\n\n\n# #region test_sync_environment_skips_resources_without_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_skips_resources_without_uuid(db_session):\n \"\"\"Resources missing uuid or having id=None should be silently skipped.\"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 42, \"slice_name\": \"No UUID\"}, # Missing 'uuid' -> skipped\n {\n \"id\": None,\n \"uuid\": \"valid-uuid\",\n \"slice_name\": \"ID is None\",\n }, # id=None -> skipped\n {\n \"id\": None,\n \"uuid\": None,\n \"slice_name\": \"Both None\",\n }, # both None -> skipped\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n count = db_session.query(ResourceMapping).count()\n assert count == 0\n\n\n# #endregion test_sync_environment_skips_resources_without_uuid\n\n\n# #region test_sync_environment_handles_api_error_gracefully [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_handles_api_error_gracefully(db_session):\n \"\"\"If one resource type fails, others should still sync.\"\"\"\n\n class FailingClient:\n def get_all_resources(self, endpoint, since_dttm=None):\n if endpoint == \"chart\":\n raise ConnectionError(\"API timeout\")\n if endpoint == \"dataset\":\n return [{\"id\": 99, \"uuid\": \"ds-uuid-1\", \"table_name\": \"users\"}]\n return []\n\n service = IdMappingService(db_session)\n service.sync_environment(\"test-env\", FailingClient())\n\n count = db_session.query(ResourceMapping).count()\n assert count == 1 # Only dataset was synced; chart error was swallowed\n mapping = db_session.query(ResourceMapping).first()\n assert mapping.resource_type == ResourceType.DATASET\n\n\n# #endregion test_sync_environment_handles_api_error_gracefully\n\n\n# #region test_get_remote_id_returns_none_for_missing [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_none_for_missing(db_session):\n \"\"\"get_remote_id should return None when no mapping exists.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_id(\"test-env\", ResourceType.CHART, \"nonexistent-uuid\")\n assert result is None\n\n\n# #endregion test_get_remote_id_returns_none_for_missing\n\n\n# #region test_get_remote_ids_batch_returns_empty_for_empty_input [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):\n \"\"\"get_remote_ids_batch should return {} for an empty list of UUIDs.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_ids_batch(\"test-env\", ResourceType.CHART, [])\n assert result == {}\n\n\n# #endregion test_get_remote_ids_batch_returns_empty_for_empty_input\n\n\n# #region test_mapping_service_alignment_with_test_data [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_mapping_service_alignment_with_test_data(db_session):\n \"\"\"**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract.\"\"\"\n # Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n contract_data = {\n \"environment_id\": \"prod-env-1\",\n \"resource_type\": ResourceType.CHART,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"remote_integer_id\": \"42\",\n }\n\n mapping = ResourceMapping(**contract_data)\n db_session.add(mapping)\n db_session.commit()\n\n service = IdMappingService(db_session)\n result = service.get_remote_id(\n contract_data[\"environment_id\"],\n contract_data[\"resource_type\"],\n contract_data[\"uuid\"],\n )\n\n assert result == 42\n\n\n# #endregion test_mapping_service_alignment_with_test_data\n\n\n# #region test_sync_environment_requires_existing_env [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_requires_existing_env(db_session):\n \"\"\"**@PRE**: Verify behavior when environment_id is invalid/missing in DB.\n Note: The current implementation doesn't strictly check for environment existencia in the DB\n before polling, but it should handle it gracefully or follow the contract.\n \"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient({\"chart\": []})\n\n # Even if environment doesn't exist in a hypothetical 'environments' table,\n # the service should still complete or fail according to defined error handling.\n # In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,\n # we simulate the intent.\n\n service.sync_environment(\"non-existent-env\", mock_client)\n # If no error raised, at least verify no mappings were created for other envs\n assert db_session.query(ResourceMapping).count() == 0\n\n\n# #endregion test_sync_environment_requires_existing_env\n\n\n# #region test_sync_environment_deletes_stale_mappings [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_deletes_stale_mappings(db_session):\n \"\"\"Verify that mappings for resources deleted from the remote environment\n are removed from the local DB on the next sync cycle.\"\"\"\n service = IdMappingService(db_session)\n\n # First sync: 2 charts exist\n client_v1 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n {\"id\": 2, \"uuid\": \"bbb\", \"slice_name\": \"Chart B\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v1)\n assert (\n db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").count() == 2\n )\n\n # Second sync: user deleted Chart B from superset\n client_v2 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v2)\n\n remaining = db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").all()\n assert len(remaining) == 1\n assert remaining[0].uuid == \"aaa\"\n\n\n# #endregion test_sync_environment_deletes_stale_mappings\n# #endregion TestMappingService\n" + "body": "# #region TestMappingService [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.\n# @LAYER Domain\n# @RELATION BINDS_TO ->[IdMappingService]\n#\nfrom datetime import UTC, datetime\nfrom pathlib import Path\nimport pytest\nimport sys\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\n\n# Add backend directory to sys.path so 'src' can be resolved\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.mapping_service import IdMappingService\nfrom src.models.mapping import Base, Environment, ResourceMapping, ResourceType\n\n\n@pytest.fixture\ndef db_session():\n # In-memory SQLite for testing\n engine = create_engine(\"sqlite:///:memory:\")\n event.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n # Create FK parent Environment records for all environment_ids used in tests\n for env_id in ['test-env', 'prod-env-1', 'env1']:\n if not session.query(Environment).filter(Environment.id == env_id).first():\n session.add(Environment(\n id=env_id, name=env_id,\n url=f'http://{env_id}.example.com',\n credentials_id=env_id\n ))\n session.commit()\n yield session\n session.close()\n\n\nclass MockSupersetClient:\n def __init__(self, resources):\n self.resources = resources\n\n def get_all_resources(self, endpoint, since_dttm=None):\n return self.resources.get(endpoint, [])\n\n\n# #region test_sync_environment_upserts_correctly [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_upserts_correctly(db_session):\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Test Chart\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = db_session.query(ResourceMapping).first()\n assert mapping is not None\n assert mapping.environment_id == \"test-env\"\n assert mapping.resource_type == ResourceType.CHART\n assert mapping.uuid == \"123e4567-e89b-12d3-a456-426614174000\"\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Test Chart\"\n\n\n# #endregion test_sync_environment_upserts_correctly\n\n\n# #region test_get_remote_id_returns_integer [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_integer(db_session):\n service = IdMappingService(db_session)\n mapping = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DATASET,\n uuid=\"uuid-1\",\n remote_integer_id=\"99\",\n resource_name=\"Test DS\",\n last_synced_at=datetime.now(UTC),\n )\n db_session.add(mapping)\n db_session.commit()\n\n result = service.get_remote_id(\"test-env\", ResourceType.DATASET, \"uuid-1\")\n assert result == 99\n\n\n# #endregion test_get_remote_id_returns_integer\n\n\n# #region test_get_remote_ids_batch_returns_dict [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_dict(db_session):\n service = IdMappingService(db_session)\n m1 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-1\",\n remote_integer_id=\"11\",\n )\n m2 = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.DASHBOARD,\n uuid=\"uuid-2\",\n remote_integer_id=\"22\",\n )\n db_session.add_all([m1, m2])\n db_session.commit()\n\n result = service.get_remote_ids_batch(\n \"test-env\", ResourceType.DASHBOARD, [\"uuid-1\", \"uuid-2\", \"uuid-missing\"]\n )\n\n assert len(result) == 2\n assert result[\"uuid-1\"] == 11\n assert result[\"uuid-2\"] == 22\n assert \"uuid-missing\" not in result\n\n\n# #endregion test_get_remote_ids_batch_returns_dict\n\n\n# #region test_sync_environment_updates_existing_mapping [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_updates_existing_mapping(db_session):\n \"\"\"Verify that sync_environment updates an existing mapping (upsert UPDATE path).\"\"\"\n from src.models.mapping import ResourceMapping\n\n # Pre-populate a mapping\n existing = ResourceMapping(\n environment_id=\"test-env\",\n resource_type=ResourceType.CHART,\n uuid=\"123e4567-e89b-12d3-a456-426614174000\",\n remote_integer_id=\"10\",\n resource_name=\"Old Name\",\n )\n db_session.add(existing)\n db_session.commit()\n\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\n \"id\": 42,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"slice_name\": \"Updated Name\",\n }\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n mapping = (\n db_session.query(ResourceMapping)\n .filter_by(uuid=\"123e4567-e89b-12d3-a456-426614174000\")\n .first()\n )\n assert mapping.remote_integer_id == \"42\"\n assert mapping.resource_name == \"Updated Name\"\n # Should still be only one record (updated, not duplicated)\n count = db_session.query(ResourceMapping).count()\n assert count == 1\n\n\n# #endregion test_sync_environment_updates_existing_mapping\n\n\n# #region test_sync_environment_skips_resources_without_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_skips_resources_without_uuid(db_session):\n \"\"\"Resources missing uuid or having id=None should be silently skipped.\"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 42, \"slice_name\": \"No UUID\"}, # Missing 'uuid' -> skipped\n {\n \"id\": None,\n \"uuid\": \"valid-uuid\",\n \"slice_name\": \"ID is None\",\n }, # id=None -> skipped\n {\n \"id\": None,\n \"uuid\": None,\n \"slice_name\": \"Both None\",\n }, # both None -> skipped\n ]\n }\n )\n\n service.sync_environment(\"test-env\", mock_client)\n\n count = db_session.query(ResourceMapping).count()\n assert count == 0\n\n\n# #endregion test_sync_environment_skips_resources_without_uuid\n\n\n# #region test_sync_environment_handles_api_error_gracefully [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_handles_api_error_gracefully(db_session):\n \"\"\"If one resource type fails, others should still sync.\"\"\"\n\n class FailingClient:\n def get_all_resources(self, endpoint, since_dttm=None):\n if endpoint == \"chart\":\n raise ConnectionError(\"API timeout\")\n if endpoint == \"dataset\":\n return [{\"id\": 99, \"uuid\": \"ds-uuid-1\", \"table_name\": \"users\"}]\n return []\n\n service = IdMappingService(db_session)\n service.sync_environment(\"test-env\", FailingClient())\n\n count = db_session.query(ResourceMapping).count()\n assert count == 1 # Only dataset was synced; chart error was swallowed\n mapping = db_session.query(ResourceMapping).first()\n assert mapping.resource_type == ResourceType.DATASET\n\n\n# #endregion test_sync_environment_handles_api_error_gracefully\n\n\n# #region test_get_remote_id_returns_none_for_missing [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_id_returns_none_for_missing(db_session):\n \"\"\"get_remote_id should return None when no mapping exists.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_id(\"test-env\", ResourceType.CHART, \"nonexistent-uuid\")\n assert result is None\n\n\n# #endregion test_get_remote_id_returns_none_for_missing\n\n\n# #region test_get_remote_ids_batch_returns_empty_for_empty_input [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):\n \"\"\"get_remote_ids_batch should return {} for an empty list of UUIDs.\"\"\"\n service = IdMappingService(db_session)\n result = service.get_remote_ids_batch(\"test-env\", ResourceType.CHART, [])\n assert result == {}\n\n\n# #endregion test_get_remote_ids_batch_returns_empty_for_empty_input\n\n\n# #region test_mapping_service_alignment_with_test_data [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_mapping_service_alignment_with_test_data(db_session):\n \"\"\"**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract.\"\"\"\n # Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}\n contract_data = {\n \"environment_id\": \"prod-env-1\",\n \"resource_type\": ResourceType.CHART,\n \"uuid\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"remote_integer_id\": \"42\",\n }\n\n mapping = ResourceMapping(**contract_data)\n db_session.add(mapping)\n db_session.commit()\n\n service = IdMappingService(db_session)\n result = service.get_remote_id(\n contract_data[\"environment_id\"],\n contract_data[\"resource_type\"],\n contract_data[\"uuid\"],\n )\n\n assert result == 42\n\n\n# #endregion test_mapping_service_alignment_with_test_data\n\n\n# #region test_sync_environment_requires_existing_env [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_requires_existing_env(db_session):\n \"\"\"**@PRE**: Verify behavior when environment_id is invalid/missing in DB.\n Note: The current implementation doesn't strictly check for environment existencia in the DB\n before polling, but it should handle it gracefully or follow the contract.\n \"\"\"\n service = IdMappingService(db_session)\n mock_client = MockSupersetClient({\"chart\": []})\n\n # Even if environment doesn't exist in a hypothetical 'environments' table,\n # the service should still complete or fail according to defined error handling.\n # In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,\n # we simulate the intent.\n\n service.sync_environment(\"non-existent-env\", mock_client)\n # If no error raised, at least verify no mappings were created for other envs\n assert db_session.query(ResourceMapping).count() == 0\n\n\n# #endregion test_sync_environment_requires_existing_env\n\n\n# #region test_sync_environment_deletes_stale_mappings [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestMappingService]\ndef test_sync_environment_deletes_stale_mappings(db_session):\n \"\"\"Verify that mappings for resources deleted from the remote environment\n are removed from the local DB on the next sync cycle.\"\"\"\n service = IdMappingService(db_session)\n\n # First sync: 2 charts exist\n client_v1 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n {\"id\": 2, \"uuid\": \"bbb\", \"slice_name\": \"Chart B\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v1)\n assert (\n db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").count() == 2\n )\n\n # Second sync: user deleted Chart B from superset\n client_v2 = MockSupersetClient(\n {\n \"chart\": [\n {\"id\": 1, \"uuid\": \"aaa\", \"slice_name\": \"Chart A\"},\n ]\n }\n )\n service.sync_environment(\"env1\", client_v2)\n\n remaining = db_session.query(ResourceMapping).filter_by(environment_id=\"env1\").all()\n assert len(remaining) == 1\n assert remaining[0].uuid == \"aaa\"\n\n\n# #endregion test_sync_environment_deletes_stale_mappings\n# #endregion TestMappingService\n" }, { "contract_id": "test_sync_environment_upserts_correctly", @@ -49901,7 +50781,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestMigrationEngine [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.\n# @LAYER Domain\n# @RELATION BINDS_TO -> [MigrationEngine]\n#\nimport json\nimport os\nimport sys\nimport tempfile\nimport zipfile\nfrom pathlib import Path\n\nimport pytest\nimport yaml\n\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration_engine import MigrationEngine\n\n# --- Fixtures ---\n\n\n# #region MockMappingService [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @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.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# #endregion MockMappingService\n\n\n# #region _write_dashboard_yaml [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Serialize dashboard metadata into YAML fixture with json_metadata payload for patch tests.\ndef _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:\n \"\"\"Helper: writes a dashboard YAML file with json_metadata.\"\"\"\n file_path = dir_path / \"dash.yaml\"\n with open(file_path, \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n return file_path\n\n\n# --- _patch_dashboard_metadata tests ---\n\n# #endregion _write_dashboard_yaml\n\n\n# #region test_patch_dashboard_metadata_replaces_chart_ids [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# #endregion test_patch_dashboard_metadata_replaces_chart_ids\n\n\n# #region test_patch_dashboard_metadata_replaces_dataset_ids [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# #endregion test_patch_dashboard_metadata_replaces_dataset_ids\n\n\n# #region test_patch_dashboard_metadata_skips_when_no_metadata [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# #endregion test_patch_dashboard_metadata_skips_when_no_metadata\n\n\n# #region test_patch_dashboard_metadata_handles_missing_targets [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# #endregion test_patch_dashboard_metadata_handles_missing_targets\n\n\n# #region test_extract_chart_uuids_from_archive [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify chart archive scan returns complete local chart id-to-uuid mapping.\ndef test_extract_chart_uuids_from_archive():\n \"\"\"Verifies that chart YAML files are parsed for id->uuid mappings.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n charts_dir = Path(td) / \"charts\"\n charts_dir.mkdir()\n\n chart1 = {\"id\": 42, \"uuid\": \"uuid-42\", \"slice_name\": \"Chart One\"}\n chart2 = {\"id\": 99, \"uuid\": \"uuid-99\", \"slice_name\": \"Chart Two\"}\n\n with open(charts_dir / \"chart1.yaml\", \"w\") as f:\n yaml.dump(chart1, f)\n with open(charts_dir / \"chart2.yaml\", \"w\") as f:\n yaml.dump(chart2, f)\n\n result = engine._extract_chart_uuids_from_archive(Path(td))\n\n assert result == {42: \"uuid-42\", 99: \"uuid-99\"}\n\n\n# --- _transform_yaml tests ---\n\n# #endregion test_extract_chart_uuids_from_archive\n\n\n# #region test_transform_yaml_replaces_database_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# #endregion test_transform_yaml_replaces_database_uuid\n\n\n# #region test_transform_yaml_ignores_unmapped_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# #endregion test_transform_yaml_ignores_unmapped_uuid\n\n\n# #region test_transform_zip_end_to_end [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# #endregion test_transform_zip_end_to_end\n\n\n# #region test_transform_zip_invalid_path [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify transform_zip returns False when source archive path does not exist.\ndef test_transform_zip_invalid_path():\n \"\"\"@PRE: Verify behavior (False) on invalid ZIP path.\"\"\"\n engine = MigrationEngine()\n success = engine.transform_zip(\"non_existent.zip\", \"output.zip\", {})\n assert success is False\n\n\n# #endregion test_transform_zip_invalid_path\n\n\n# #region test_transform_yaml_nonexistent_file [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify transform_yaml raises FileNotFoundError for missing YAML source files.\ndef test_transform_yaml_nonexistent_file():\n \"\"\"@PRE: Verify behavior on non-existent YAML file.\"\"\"\n engine = MigrationEngine()\n # Should log error and not crash (implemented via try-except if wrapped,\n # but _transform_yaml itself might raise FileNotFoundError if not guarded)\n with pytest.raises(FileNotFoundError):\n engine._transform_yaml(Path(\"non_existent.yaml\"), {})\n\n\n# #endregion test_transform_yaml_nonexistent_file\n# #endregion TestMigrationEngine\n" + "body": "# #region TestMigrationEngine [C:2] [TYPE Module]\n#\n# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.\n# @LAYER Domain\n# @RELATION BINDS_TO -> [MigrationEngine]\n#\nimport json\nimport os\nfrom pathlib import Path\nimport pytest\nimport sys\nimport tempfile\nimport zipfile\n\nimport yaml\n\nbackend_dir = str(Path(__file__).parent.parent.parent.resolve())\nif backend_dir not in sys.path:\n sys.path.insert(0, backend_dir)\n\nfrom src.core.migration_engine import MigrationEngine\n\n# --- Fixtures ---\n\n\n# #region MockMappingService [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.\n# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.\nclass MockMappingService:\n \"\"\"Mock that simulates IdMappingService.get_remote_ids_batch.\"\"\"\n\n def __init__(self, mappings: dict):\n self.mappings = mappings\n\n def get_remote_ids_batch(self, env_id, resource_type, uuids):\n # @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.\n result = {}\n for uuid in uuids:\n if uuid in self.mappings:\n result[uuid] = self.mappings[uuid]\n return result\n\n\n# #endregion MockMappingService\n\n\n# #region _write_dashboard_yaml [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Serialize dashboard metadata into YAML fixture with json_metadata payload for patch tests.\ndef _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:\n \"\"\"Helper: writes a dashboard YAML file with json_metadata.\"\"\"\n file_path = dir_path / \"dash.yaml\"\n with open(file_path, \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n return file_path\n\n\n# --- _patch_dashboard_metadata tests ---\n\n# #endregion _write_dashboard_yaml\n\n\n# #region test_patch_dashboard_metadata_replaces_chart_ids [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify native filter target chartId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_chart_ids():\n \"\"\"Verifies that chartId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-chart-A\": 999})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 42}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {42: \"uuid-chart-A\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"] == 999\n )\n\n\n# #endregion test_patch_dashboard_metadata_replaces_chart_ids\n\n\n# #region test_patch_dashboard_metadata_replaces_dataset_ids [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify native filter target datasetId values are remapped via mapping service results.\ndef test_patch_dashboard_metadata_replaces_dataset_ids():\n \"\"\"Verifies that datasetId values are replaced using the mapping service.\"\"\"\n mock_service = MockMappingService({\"uuid-ds-B\": 500})\n engine = MigrationEngine(mock_service)\n\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"datasetId\": 10}]}]}\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {10: \"uuid-ds-B\"}\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n assert (\n result[\"native_filter_configuration\"][0][\"targets\"][0][\"datasetId\"]\n == 500\n )\n\n\n# #endregion test_patch_dashboard_metadata_replaces_dataset_ids\n\n\n# #region test_patch_dashboard_metadata_skips_when_no_metadata [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure dashboard files without json_metadata are left unchanged by metadata patching.\ndef test_patch_dashboard_metadata_skips_when_no_metadata():\n \"\"\"Verifies early return when json_metadata key is absent.\"\"\"\n mock_service = MockMappingService({})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dash.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"title\": \"No metadata here\"}, f)\n\n engine._patch_dashboard_metadata(fp, \"target-env\", {})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert \"json_metadata\" not in data\n\n\n# #endregion test_patch_dashboard_metadata_skips_when_no_metadata\n\n\n# #region test_patch_dashboard_metadata_handles_missing_targets [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify patching updates mapped targets while preserving unmapped native filter IDs.\ndef test_patch_dashboard_metadata_handles_missing_targets():\n \"\"\"When some source IDs have no target mapping, patches what it can and leaves the rest.\"\"\"\n mock_service = MockMappingService({\"uuid-A\": 100}) # Only uuid-A maps\n engine = MigrationEngine(mock_service)\n\n metadata = {\n \"native_filter_configuration\": [\n {\"targets\": [{\"datasetId\": 1}, {\"datasetId\": 2}]}\n ]\n }\n\n with tempfile.TemporaryDirectory() as td:\n fp = _write_dashboard_yaml(Path(td), metadata)\n source_map = {1: \"uuid-A\", 2: \"uuid-MISSING\"} # uuid-MISSING won't resolve\n\n engine._patch_dashboard_metadata(fp, \"target-env\", source_map)\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n result = json.loads(data[\"json_metadata\"])\n targets = result[\"native_filter_configuration\"][0][\"targets\"]\n # ID 1 should be replaced to 100; ID 2 should remain 2\n assert targets[0][\"datasetId\"] == 100\n assert targets[1][\"datasetId\"] == 2\n\n\n# --- _extract_chart_uuids_from_archive tests ---\n\n# #endregion test_patch_dashboard_metadata_handles_missing_targets\n\n\n# #region test_extract_chart_uuids_from_archive [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify chart archive scan returns complete local chart id-to-uuid mapping.\ndef test_extract_chart_uuids_from_archive():\n \"\"\"Verifies that chart YAML files are parsed for id->uuid mappings.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n charts_dir = Path(td) / \"charts\"\n charts_dir.mkdir()\n\n chart1 = {\"id\": 42, \"uuid\": \"uuid-42\", \"slice_name\": \"Chart One\"}\n chart2 = {\"id\": 99, \"uuid\": \"uuid-99\", \"slice_name\": \"Chart Two\"}\n\n with open(charts_dir / \"chart1.yaml\", \"w\") as f:\n yaml.dump(chart1, f)\n with open(charts_dir / \"chart2.yaml\", \"w\") as f:\n yaml.dump(chart2, f)\n\n result = engine._extract_chart_uuids_from_archive(Path(td))\n\n assert result == {42: \"uuid-42\", 99: \"uuid-99\"}\n\n\n# --- _transform_yaml tests ---\n\n# #endregion test_extract_chart_uuids_from_archive\n\n\n# #region test_transform_yaml_replaces_database_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure dataset YAML database_uuid fields are replaced when source UUID mapping exists.\ndef test_transform_yaml_replaces_database_uuid():\n \"\"\"Verifies that database_uuid in a dataset YAML is replaced.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n with open(fp, \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-uuid-abc\", \"table_name\": \"my_table\"}, f)\n\n engine._transform_yaml(fp, {\"source-uuid-abc\": \"target-uuid-xyz\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"target-uuid-xyz\"\n assert data[\"table_name\"] == \"my_table\"\n\n\n# #endregion test_transform_yaml_replaces_database_uuid\n\n\n# #region test_transform_yaml_ignores_unmapped_uuid [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Ensure transform_yaml leaves dataset files untouched when database_uuid is not mapped.\ndef test_transform_yaml_ignores_unmapped_uuid():\n \"\"\"Verifies no changes when UUID is not in the mapping.\"\"\"\n engine = MigrationEngine()\n\n with tempfile.TemporaryDirectory() as td:\n fp = Path(td) / \"dataset.yaml\"\n original = {\"database_uuid\": \"unknown-uuid\", \"table_name\": \"test\"}\n with open(fp, \"w\") as f:\n yaml.dump(original, f)\n\n engine._transform_yaml(fp, {\"other-uuid\": \"replacement\"})\n\n with open(fp) as f:\n data = yaml.safe_load(f)\n assert data[\"database_uuid\"] == \"unknown-uuid\"\n\n\n# --- [NEW] transform_zip E2E tests ---\n\n# #endregion test_transform_yaml_ignores_unmapped_uuid\n\n\n# #region test_transform_zip_end_to_end [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Validate full ZIP transform pipeline remaps datasets and dashboard cross-filter chart IDs.\ndef test_transform_zip_end_to_end():\n \"\"\"Verifies full orchestration: extraction, transformation, patching, and re-packaging.\"\"\"\n mock_service = MockMappingService({\"char-uuid\": 101, \"ds-uuid\": 202})\n engine = MigrationEngine(mock_service)\n\n with tempfile.TemporaryDirectory() as td:\n td_path = Path(td)\n zip_path = td_path / \"source.zip\"\n output_path = td_path / \"target.zip\"\n\n # Create source ZIP structure\n with tempfile.TemporaryDirectory() as src_dir:\n src_path = Path(src_dir)\n\n # 1. Dataset\n ds_dir = src_path / \"datasets\"\n ds_dir.mkdir()\n with open(ds_dir / \"ds.yaml\", \"w\") as f:\n yaml.dump({\"database_uuid\": \"source-db-uuid\", \"table_name\": \"users\"}, f)\n\n # 2. Chart\n ch_dir = src_path / \"charts\"\n ch_dir.mkdir()\n with open(ch_dir / \"ch.yaml\", \"w\") as f:\n yaml.dump({\"id\": 10, \"uuid\": \"char-uuid\"}, f)\n\n # 3. Dashboard\n db_dir = src_path / \"dashboards\"\n db_dir.mkdir()\n metadata = {\"native_filter_configuration\": [{\"targets\": [{\"chartId\": 10}]}]}\n with open(db_dir / \"db.yaml\", \"w\") as f:\n yaml.dump({\"json_metadata\": json.dumps(metadata)}, f)\n\n with zipfile.ZipFile(zip_path, \"w\") as zf:\n for root, _, files in os.walk(src_path):\n for file in files:\n p = Path(root) / file\n zf.write(p, p.relative_to(src_path))\n\n db_mapping = {\"source-db-uuid\": \"target-db-uuid\"}\n\n # Execute transform\n success = engine.transform_zip(\n str(zip_path),\n str(output_path),\n db_mapping,\n target_env_id=\"test-target\",\n fix_cross_filters=True,\n )\n\n assert success is True\n assert output_path.exists()\n\n # Verify contents\n with tempfile.TemporaryDirectory() as out_dir:\n with zipfile.ZipFile(output_path, \"r\") as zf:\n zf.extractall(out_dir)\n\n out_path = Path(out_dir)\n\n # Verify dataset transformation\n with open(out_path / \"datasets\" / \"ds.yaml\") as f:\n ds_data = yaml.safe_load(f)\n assert ds_data[\"database_uuid\"] == \"target-db-uuid\"\n\n # Verify dashboard patching\n with open(out_path / \"dashboards\" / \"db.yaml\") as f:\n db_data = yaml.safe_load(f)\n meta = json.loads(db_data[\"json_metadata\"])\n assert (\n meta[\"native_filter_configuration\"][0][\"targets\"][0][\"chartId\"]\n == 101\n )\n\n\n# #endregion test_transform_zip_end_to_end\n\n\n# #region test_transform_zip_invalid_path [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify transform_zip returns False when source archive path does not exist.\ndef test_transform_zip_invalid_path():\n \"\"\"@PRE: Verify behavior (False) on invalid ZIP path.\"\"\"\n engine = MigrationEngine()\n success = engine.transform_zip(\"non_existent.zip\", \"output.zip\", {})\n assert success is False\n\n\n# #endregion test_transform_zip_invalid_path\n\n\n# #region test_transform_yaml_nonexistent_file [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [TestMigrationEngine]\n# @PURPOSE: Verify transform_yaml raises FileNotFoundError for missing YAML source files.\ndef test_transform_yaml_nonexistent_file():\n \"\"\"@PRE: Verify behavior on non-existent YAML file.\"\"\"\n engine = MigrationEngine()\n # Should log error and not crash (implemented via try-except if wrapped,\n # but _transform_yaml itself might raise FileNotFoundError if not guarded)\n with pytest.raises(FileNotFoundError):\n engine._transform_yaml(Path(\"non_existent.yaml\"), {})\n\n\n# #endregion test_transform_yaml_nonexistent_file\n# #endregion TestMigrationEngine\n" }, { "contract_id": "MockMappingService", @@ -50337,7 +51217,7 @@ "contract_type": "Module", "file_path": "backend/tests/scripts/test_clean_release_tui.py", "start_line": 1, - "end_line": 245, + "end_line": 244, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50363,14 +51243,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestCleanReleaseTui [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, tui, clean-release, curses\n# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.\n# @LAYER Tests\n# @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.\n\nimport curses\nimport json\nimport os\nfrom unittest import mock\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.models.clean_release import CheckFinalStatus\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main\n\n\n@pytest.fixture\ndef mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 100)\n stdscr.getch.return_value = -1\n return stdscr\n\n\n# #region test_headless_fallback [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_headless_fallback(capsys):\n \"\"\"\n @TEST_EDGE: stdout_unavailable\n Tests that non-TTY startup is explicitly refused and wrapper is not invoked.\n \"\"\"\n with mock.patch(\"src.scripts.clean_release_tui.curses.wrapper\") as curses_wrapper_mock:\n with mock.patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n curses_wrapper_mock.assert_not_called()\n\n assert exit_code == 2\n captured = capsys.readouterr()\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# #endregion test_headless_fallback\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_initial_render [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n\n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n app.loop()\n\n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n\n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# #endregion test_tui_initial_render\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_run_checks_f5 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n\n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n\n mock_stdscr.getch.side_effect = side_effect\n\n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n\n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# #endregion test_tui_run_checks_f5\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_exit_f10 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n\n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n # loop() should return cleanly\n app.loop()\n\n assert app.status == \"READY\"\n\n\n# #endregion test_tui_exit_f10\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_clear_history_f7 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n\n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n\n app.loop()\n\n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# #endregion test_tui_clear_history_f7\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_real_mode_bootstrap_imports_artifacts_catalog [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_real_mode_bootstrap_imports_artifacts_catalog(\n mock_curses_module,\n mock_stdscr: MagicMock,\n tmp_path,\n# #endregion test_tui_real_mode_bootstrap_imports_artifacts_catalog\n\n):\n \"\"\"\n @TEST_CONTRACT: bootstrap.json + artifacts.json -> candidate PREPARED with imported artifacts\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n bootstrap_path = tmp_path / \"bootstrap.json\"\n artifacts_path = tmp_path / \"artifacts.json\"\n bootstrap_path.write_text(\n json.dumps(\n {\n \"candidate_id\": \"real-candidate-1\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:release/1\",\n \"created_by\": \"operator\",\n \"allowed_hosts\": [\"repo.intra.company.local\"],\n }\n ),\n encoding=\"utf-8\",\n )\n artifacts_path.write_text(\n json.dumps(\n {\n \"artifacts\": [\n {\n \"id\": \"artifact-1\",\n \"path\": \"backend/dist/package.tar.gz\",\n \"sha256\": \"deadbeef\",\n \"size\": 1024,\n \"category\": \"core\",\n \"source_uri\": \"https://repo.intra.company.local/releases/package.tar.gz\",\n \"source_host\": \"repo.intra.company.local\",\n }\n ]\n }\n ),\n encoding=\"utf-8\",\n )\n\n with mock.patch.dict(\n os.environ,\n {\n \"CLEAN_TUI_MODE\": \"real\",\n \"CLEAN_TUI_BOOTSTRAP_JSON\": str(bootstrap_path),\n \"CLEAN_TUI_ARTIFACTS_JSON\": str(artifacts_path),\n },\n clear=False,\n ):\n app = CleanReleaseTUI(mock_stdscr)\n\n candidate = app.repo.get_candidate(\"real-candidate-1\")\n artifacts = app.repo.get_artifacts_by_candidate(\"real-candidate-1\")\n\n assert candidate is not None\n assert candidate.status == \"PREPARED\"\n assert len(artifacts) == 1\n assert artifacts[0].path == \"backend/dist/package.tar.gz\"\n assert artifacts[0].detected_category == \"core\"\n\n\n# #endregion TestCleanReleaseTui\n" + "body": "# #region TestCleanReleaseTui [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, tui, clean-release, curses\n# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.\n# @LAYER Tests\n# @INVARIANT: TUI initializes, handles hotkeys (F5, F10) and safely falls back without TTY.\n\nimport curses\nimport json\nimport os\nimport pytest\nfrom unittest import mock\nfrom unittest.mock import MagicMock, patch\n\nfrom src.models.clean_release import CheckFinalStatus\nfrom src.scripts.clean_release_tui import CleanReleaseTUI, main\n\n\n@pytest.fixture\ndef mock_stdscr() -> MagicMock:\n stdscr = MagicMock()\n stdscr.getmaxyx.return_value = (40, 100)\n stdscr.getch.return_value = -1\n return stdscr\n\n\n# #region test_headless_fallback [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_headless_fallback(capsys):\n \"\"\"\n @TEST_EDGE: stdout_unavailable\n Tests that non-TTY startup is explicitly refused and wrapper is not invoked.\n \"\"\"\n with mock.patch(\"src.scripts.clean_release_tui.curses.wrapper\") as curses_wrapper_mock:\n with mock.patch(\"sys.stdout.isatty\", return_value=False):\n exit_code = main()\n\n curses_wrapper_mock.assert_not_called()\n\n assert exit_code == 2\n captured = capsys.readouterr()\n assert \"TTY is required for TUI mode\" in captured.err\n assert \"Use CLI/API workflow instead\" in captured.err\n\n\n# #endregion test_headless_fallback\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_initial_render [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_initial_render(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates the initial rendering cycle of the TUI application to ensure\n titles, headers, footers and the READY state are drawn appropriately.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n assert app.status == \"READY\"\n\n # We only want to run one loop iteration, so we mock getch to return F10\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n app.loop()\n\n # Assert header was drawn\n addstr_calls = mock_stdscr.addstr.call_args_list\n assert any(\"Enterprise Clean Release Validator\" in str(call) for call in addstr_calls)\n assert any(\"Candidate: [2026.03.03-rc1]\" in str(call) for call in addstr_calls)\n\n # Assert checks list is shown\n assert any(\"Data Purity\" in str(call) for call in addstr_calls)\n assert any(\"Internal Sources Only\" in str(call) for call in addstr_calls)\n\n # Assert footer is shown\n assert any(\"F5 Run\" in str(call) for call in addstr_calls)\n\n\n# #endregion test_tui_initial_render\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_run_checks_f5 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_run_checks_f5(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F5 to transition into the RUNNING checks flow.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F5 = curses.KEY_F5\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n\n # getch sequence:\n # 1. First loop: F5 (triggers run_checks)\n # 2. Next call after run_checks: F10 to exit\n mock_stdscr.f5_pressed = False\n def side_effect():\n if not mock_stdscr.f5_pressed:\n mock_stdscr.f5_pressed = True\n return curses.KEY_F5\n return curses.KEY_F10\n\n mock_stdscr.getch.side_effect = side_effect\n\n with mock.patch(\"time.sleep\", return_value=None):\n app.loop()\n\n # After F5 is pressed, status should be BLOCKED due to deliberate 'test-data' violation\n assert app.status == CheckFinalStatus.BLOCKED\n assert app.report_id is not None\n assert \"CCR-\" in app.report_id\n assert len(app.violations_list) > 0\n\n\n# #endregion test_tui_run_checks_f5\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_exit_f10 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_exit_f10(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F10 to exit the application immediately without running checks.\n \"\"\"\n # Ensure constants match\n mock_curses_module.KEY_F10 = curses.KEY_F10\n\n app = CleanReleaseTUI(mock_stdscr)\n mock_stdscr.getch.return_value = curses.KEY_F10\n\n # loop() should return cleanly\n app.loop()\n\n assert app.status == \"READY\"\n\n\n# #endregion test_tui_exit_f10\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_clear_history_f7 [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_clear_history_f7(mock_curses_module, mock_stdscr: MagicMock):\n \"\"\"\n Simulates pressing F7 to clear history.\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.KEY_F7 = curses.KEY_F7\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n app = CleanReleaseTUI(mock_stdscr)\n app.status = CheckFinalStatus.BLOCKED\n app.report_id = \"SOME-REPORT\"\n\n # F7 then F10\n mock_stdscr.getch.side_effect = [curses.KEY_F7, curses.KEY_F10]\n\n app.loop()\n\n assert app.status == \"READY\"\n assert app.report_id is None\n assert len(app.checks_progress) == 0\n\n\n# #endregion test_tui_clear_history_f7\n\n@patch(\"src.scripts.clean_release_tui.curses\")\n# #region test_tui_real_mode_bootstrap_imports_artifacts_catalog [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestCleanReleaseTui\ndef test_tui_real_mode_bootstrap_imports_artifacts_catalog(\n mock_curses_module,\n mock_stdscr: MagicMock,\n tmp_path,\n# #endregion test_tui_real_mode_bootstrap_imports_artifacts_catalog\n\n):\n \"\"\"\n @TEST_CONTRACT: bootstrap.json + artifacts.json -> candidate PREPARED with imported artifacts\n \"\"\"\n mock_curses_module.KEY_F10 = curses.KEY_F10\n mock_curses_module.color_pair.side_effect = lambda x: x\n mock_curses_module.A_BOLD = 0\n\n bootstrap_path = tmp_path / \"bootstrap.json\"\n artifacts_path = tmp_path / \"artifacts.json\"\n bootstrap_path.write_text(\n json.dumps(\n {\n \"candidate_id\": \"real-candidate-1\",\n \"version\": \"1.0.0\",\n \"source_snapshot_ref\": \"git:release/1\",\n \"created_by\": \"operator\",\n \"allowed_hosts\": [\"repo.intra.company.local\"],\n }\n ),\n encoding=\"utf-8\",\n )\n artifacts_path.write_text(\n json.dumps(\n {\n \"artifacts\": [\n {\n \"id\": \"artifact-1\",\n \"path\": \"backend/dist/package.tar.gz\",\n \"sha256\": \"deadbeef\",\n \"size\": 1024,\n \"category\": \"core\",\n \"source_uri\": \"https://repo.intra.company.local/releases/package.tar.gz\",\n \"source_host\": \"repo.intra.company.local\",\n }\n ]\n }\n ),\n encoding=\"utf-8\",\n )\n\n with mock.patch.dict(\n os.environ,\n {\n \"CLEAN_TUI_MODE\": \"real\",\n \"CLEAN_TUI_BOOTSTRAP_JSON\": str(bootstrap_path),\n \"CLEAN_TUI_ARTIFACTS_JSON\": str(artifacts_path),\n },\n clear=False,\n ):\n app = CleanReleaseTUI(mock_stdscr)\n\n candidate = app.repo.get_candidate(\"real-candidate-1\")\n artifacts = app.repo.get_artifacts_by_candidate(\"real-candidate-1\")\n\n assert candidate is not None\n assert candidate.status == \"PREPARED\"\n assert len(artifacts) == 1\n assert artifacts[0].path == \"backend/dist/package.tar.gz\"\n assert artifacts[0].detected_category == \"core\"\n\n\n# #endregion TestCleanReleaseTui\n" }, { "contract_id": "test_headless_fallback", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 28, - "end_line": 47, + "start_line": 27, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50393,8 +51273,8 @@ "contract_id": "test_tui_initial_render", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 50, - "end_line": 84, + "start_line": 49, + "end_line": 83, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50417,8 +51297,8 @@ "contract_id": "test_tui_run_checks_f5", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 87, - "end_line": 123, + "start_line": 86, + "end_line": 122, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50441,8 +51321,8 @@ "contract_id": "test_tui_exit_f10", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 126, - "end_line": 144, + "start_line": 125, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50465,8 +51345,8 @@ "contract_id": "test_tui_clear_history_f7", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 147, - "end_line": 172, + "start_line": 146, + "end_line": 171, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50489,8 +51369,8 @@ "contract_id": "test_tui_real_mode_bootstrap_imports_artifacts_catalog", "contract_type": "Function", "file_path": "backend/tests/scripts/test_clean_release_tui.py", - "start_line": 175, - "end_line": 181, + "start_line": 174, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50665,7 +51545,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_approval_service.py", "start_line": 1, - "end_line": 202, + "end_line": 201, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50692,14 +51572,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestApprovalService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, approval, lifecycle, gate\n# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.\n# @LAYER Tests\n# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision\nfrom src.services.clean_release.exceptions import ApprovalGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_candidate_with_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# #endregion _seed_candidate_with_report\n\n\n# #region test_approve_rejects_blocked_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.\n# @PRE: Candidate has BLOCKED report.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_blocked_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report(\n report_status=ComplianceDecision.BLOCKED,\n )\n\n with pytest.raises(ApprovalGateError, match=\"PASSED\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"blocked report cannot be approved\",\n )\n# #endregion test_approve_rejects_blocked_report\n\n\n# #region test_approve_rejects_foreign_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# #endregion test_approve_rejects_foreign_report\n\n\n# #region test_approve_rejects_duplicate_approve [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.\n# @PRE: Candidate has already been approved once.\n# @POST: Second approve_candidate call raises ApprovalGateError.\ndef test_approve_rejects_duplicate_approve():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n first = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"first approval\",\n )\n assert first.decision == ApprovalDecisionType.APPROVED.value\n assert repository.get_candidate(candidate_id).status == CandidateStatus.APPROVED.value\n\n with pytest.raises(ApprovalGateError, match=\"already approved\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"duplicate approval\",\n )\n# #endregion test_approve_rejects_duplicate_approve\n\n\n# #region test_reject_persists_decision_without_promoting_candidate_state [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.\n# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.\n# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.\ndef test_reject_persists_decision_without_promoting_candidate_state():\n from src.services.clean_release.approval_service import reject_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"manual rejection\",\n )\n\n candidate = repository.get_candidate(candidate_id)\n assert decision.decision == ApprovalDecisionType.REJECTED.value\n assert candidate is not None\n assert candidate.status == CandidateStatus.CHECK_PASSED.value\n# #endregion test_reject_persists_decision_without_promoting_candidate_state\n\n\n# #region test_reject_then_publish_is_blocked [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# #endregion test_reject_then_publish_is_blocked\n\n# #endregion TestApprovalService\n" + "body": "# #region TestApprovalService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, approval, lifecycle, gate\n# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.\n# @LAYER Tests\n# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; duplicate approve and foreign report must be rejected.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import ApprovalDecisionType, CandidateStatus, ComplianceDecision\nfrom src.services.clean_release.exceptions import ApprovalGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_candidate_with_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Seed candidate and report fixtures for approval gate tests.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and report linked by candidate_id.\ndef _seed_candidate_with_report(\n *,\n candidate_id: str = \"cand-approve-1\",\n report_id: str = \"CCR-approve-1\",\n report_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-approve-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.CHECK_PASSED.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-approve-1\",\n candidate_id=candidate_id,\n final_status=report_status.value,\n summary_json={\n \"operator_summary\": \"seed\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0 if report_status == ComplianceDecision.PASSED else 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# #endregion _seed_candidate_with_report\n\n\n# #region test_approve_rejects_blocked_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when latest report final status is not PASSED.\n# @PRE: Candidate has BLOCKED report.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_blocked_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report(\n report_status=ComplianceDecision.BLOCKED,\n )\n\n with pytest.raises(ApprovalGateError, match=\"PASSED\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"blocked report cannot be approved\",\n )\n# #endregion test_approve_rejects_blocked_report\n\n\n# #region test_approve_rejects_foreign_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure approve is rejected when report belongs to another candidate.\n# @PRE: Candidate exists, report candidate_id differs.\n# @POST: approve_candidate raises ApprovalGateError.\ndef test_approve_rejects_foreign_report():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, _ = _seed_candidate_with_report()\n foreign_report = ComplianceReport(\n id=\"CCR-foreign-1\",\n run_id=\"run-foreign-1\",\n candidate_id=\"cand-foreign-1\",\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"foreign\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n repository.save_report(foreign_report)\n\n with pytest.raises(ApprovalGateError, match=\"belongs to another candidate\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=foreign_report.id,\n decided_by=\"approver\",\n comment=\"foreign report\",\n )\n# #endregion test_approve_rejects_foreign_report\n\n\n# #region test_approve_rejects_duplicate_approve [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure repeated approve decision for same candidate is blocked.\n# @PRE: Candidate has already been approved once.\n# @POST: Second approve_candidate call raises ApprovalGateError.\ndef test_approve_rejects_duplicate_approve():\n from src.services.clean_release.approval_service import approve_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n first = approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"first approval\",\n )\n assert first.decision == ApprovalDecisionType.APPROVED.value\n assert repository.get_candidate(candidate_id).status == CandidateStatus.APPROVED.value\n\n with pytest.raises(ApprovalGateError, match=\"already approved\"):\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"duplicate approval\",\n )\n# #endregion test_approve_rejects_duplicate_approve\n\n\n# #region test_reject_persists_decision_without_promoting_candidate_state [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure reject decision is immutable and does not promote candidate to APPROVED.\n# @PRE: Candidate has PASSED report and CHECK_PASSED lifecycle state.\n# @POST: reject_candidate persists REJECTED decision; candidate status remains unchanged.\ndef test_reject_persists_decision_without_promoting_candidate_state():\n from src.services.clean_release.approval_service import reject_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n decision = reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"manual rejection\",\n )\n\n candidate = repository.get_candidate(candidate_id)\n assert decision.decision == ApprovalDecisionType.REJECTED.value\n assert candidate is not None\n assert candidate.status == CandidateStatus.CHECK_PASSED.value\n# #endregion test_reject_persists_decision_without_promoting_candidate_state\n\n\n# #region test_reject_then_publish_is_blocked [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestApprovalService\n# @PURPOSE: Ensure latest REJECTED decision blocks publication gate.\n# @PRE: Candidate is rejected for passed report.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_reject_then_publish_is_blocked():\n from src.services.clean_release.approval_service import reject_candidate\n from src.services.clean_release.exceptions import PublicationGateError\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_report()\n\n reject_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"rejected before publish\",\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-blocked\",\n )\n# #endregion test_reject_then_publish_is_blocked\n\n# #endregion TestApprovalService\n" }, { "contract_id": "_seed_candidate_with_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 20, - "end_line": 58, + "start_line": 19, + "end_line": 57, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50725,8 +51605,8 @@ "contract_id": "test_approve_rejects_blocked_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 61, - "end_line": 81, + "start_line": 60, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50752,8 +51632,8 @@ "contract_id": "test_approve_rejects_foreign_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 84, - "end_line": 112, + "start_line": 83, + "end_line": 111, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50779,8 +51659,8 @@ "contract_id": "test_approve_rejects_duplicate_approve", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 115, - "end_line": 143, + "start_line": 114, + "end_line": 142, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50806,8 +51686,8 @@ "contract_id": "test_reject_persists_decision_without_promoting_candidate_state", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 146, - "end_line": 168, + "start_line": 145, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50833,8 +51713,8 @@ "contract_id": "test_reject_then_publish_is_blocked", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_approval_service.py", - "start_line": 171, - "end_line": 200, + "start_line": 170, + "end_line": 199, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -50880,7 +51760,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_candidate_manifest_services [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> [SrcRoot]\n# @PURPOSE: Test lifecycle and manifest versioning for release candidates.\n# @LAYER Tests\n\nfrom datetime import UTC, datetime\n\nimport pytest\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.database import Base\nfrom src.models.clean_release import (\n DistributionManifest,\n ReleaseCandidate,\n)\nfrom src.services.clean_release.candidate_service import register_candidate\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\")\n event.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n # Create FK parent for test_manifest_versioning_and_immutability\n existing = session.query(ReleaseCandidate).filter(ReleaseCandidate.id == 'test-candidate-2').first()\n if not existing:\n session.add(ReleaseCandidate(\n id='test-candidate-2', version='1.0.0',\n source_snapshot_ref='ref1', created_by='operator',\n status=CandidateStatus.DRAFT\n ))\n session.commit()\n yield session\n session.close()\n\n\n# #region test_candidate_lifecycle_transitions [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify release candidate allows legal status transitions and rejects forbidden back-transitions.\ndef test_candidate_lifecycle_transitions(db_session):\n \"\"\"\n @PURPOSE: Verify legal state transitions for ReleaseCandidate.\n \"\"\"\n candidate = ReleaseCandidate(\n id=\"test-candidate-1\",\n name=\"Test Candidate\",\n version=\"1.0.0\",\n source_snapshot_ref=\"ref-1\",\n created_by=\"operator\",\n status=CandidateStatus.DRAFT,\n )\n db_session.add(candidate)\n db_session.commit()\n\n # Valid transition: DRAFT -> PREPARED\n candidate.transition_to(CandidateStatus.PREPARED)\n assert candidate.status == CandidateStatus.PREPARED\n\n # Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)\n from src.services.clean_release.exceptions import IllegalTransitionError\n\n with pytest.raises(IllegalTransitionError, match=\"Forbidden transition\"):\n candidate.transition_to(CandidateStatus.DRAFT)\n\n\n# #endregion test_candidate_lifecycle_transitions\n\n\n# #region test_manifest_versioning_and_immutability [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# #endregion test_manifest_versioning_and_immutability\n\n\n# #region _valid_artifacts [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Provide canonical valid artifact payload used by candidate registration tests.\ndef _valid_artifacts():\n return [\n {\n \"id\": \"art-1\",\n \"path\": \"bin/app\",\n \"sha256\": \"abc123\",\n \"size\": 42,\n }\n ]\n\n\n# #endregion _valid_artifacts\n\n\n# #region test_register_candidate_rejects_duplicate_candidate_id [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify duplicate candidate_id registration is rejected by service invariants.\ndef test_register_candidate_rejects_duplicate_candidate_id():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n with pytest.raises(ValueError, match=\"already exists\"):\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n\n# #endregion test_register_candidate_rejects_duplicate_candidate_id\n\n\n# #region test_register_candidate_rejects_malformed_artifact_input [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify candidate registration rejects artifact payloads missing required fields.\ndef test_register_candidate_rejects_malformed_artifact_input():\n repository = CleanReleaseRepository()\n bad_artifacts = [{\"id\": \"art-1\", \"path\": \"bin/app\", \"size\": 42}] # missing sha256\n\n with pytest.raises(ValueError, match=\"missing required field 'sha256'\"):\n register_candidate(\n repository=repository,\n candidate_id=\"bad-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha2\",\n created_by=\"operator\",\n artifacts=bad_artifacts,\n )\n\n\n# #endregion test_register_candidate_rejects_malformed_artifact_input\n\n\n# #region test_register_candidate_rejects_empty_artifact_set [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify candidate registration rejects empty artifact collections.\ndef test_register_candidate_rejects_empty_artifact_set():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"artifacts must not be empty\"):\n register_candidate(\n repository=repository,\n candidate_id=\"empty-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha3\",\n created_by=\"operator\",\n artifacts=[],\n )\n\n\n# #endregion test_register_candidate_rejects_empty_artifact_set\n\n\n# #region test_manifest_service_rebuild_creates_new_version [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify repeated manifest build creates a new incremented immutable version.\ndef test_manifest_service_rebuild_creates_new_version():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-version-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha10\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n first = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n second = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n\n assert first.manifest_version == 1\n assert second.manifest_version == 2\n assert first.id != second.id\n\n\n# #endregion test_manifest_service_rebuild_creates_new_version\n\n\n# #region test_manifest_service_existing_manifest_cannot_be_mutated [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify existing manifest snapshot remains immutable when rebuilding newer manifest version.\ndef test_manifest_service_existing_manifest_cannot_be_mutated():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha11\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n created = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n original_digest = created.manifest_digest\n\n rebuilt = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n old_manifest = repository.get_manifest(created.id)\n\n assert old_manifest is not None\n assert old_manifest.manifest_digest == original_digest\n assert old_manifest.id == created.id\n assert rebuilt.id != created.id\n\n\n# #endregion test_manifest_service_existing_manifest_cannot_be_mutated\n\n\n# #region test_manifest_service_rejects_missing_candidate [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify manifest build fails with missing candidate identifier.\ndef test_manifest_service_rejects_missing_candidate():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"not found\"):\n build_manifest_snapshot(\n repository=repository,\n candidate_id=\"missing-candidate\",\n created_by=\"operator\",\n )\n\n\n# #endregion test_manifest_service_rejects_missing_candidate\n# #endregion test_candidate_manifest_services\n" + "body": "# #region test_candidate_manifest_services [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> [SrcRoot]\n# @PURPOSE: Test lifecycle and manifest versioning for release candidates.\n# @LAYER Tests\n\nfrom datetime import UTC, datetime\nimport pytest\n\nfrom sqlalchemy import create_engine, event\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.database import Base\nfrom src.models.clean_release import (\n DistributionManifest,\n ReleaseCandidate,\n)\nfrom src.services.clean_release.candidate_service import register_candidate\nfrom src.services.clean_release.enums import CandidateStatus\nfrom src.services.clean_release.manifest_service import build_manifest_snapshot\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n@pytest.fixture\ndef db_session():\n engine = create_engine(\"sqlite:///:memory:\")\n event.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n # Create FK parent for test_manifest_versioning_and_immutability\n existing = session.query(ReleaseCandidate).filter(ReleaseCandidate.id == 'test-candidate-2').first()\n if not existing:\n session.add(ReleaseCandidate(\n id='test-candidate-2', version='1.0.0',\n source_snapshot_ref='ref1', created_by='operator',\n status=CandidateStatus.DRAFT\n ))\n session.commit()\n yield session\n session.close()\n\n\n# #region test_candidate_lifecycle_transitions [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify release candidate allows legal status transitions and rejects forbidden back-transitions.\ndef test_candidate_lifecycle_transitions(db_session):\n \"\"\"\n @PURPOSE: Verify legal state transitions for ReleaseCandidate.\n \"\"\"\n candidate = ReleaseCandidate(\n id=\"test-candidate-1\",\n name=\"Test Candidate\",\n version=\"1.0.0\",\n source_snapshot_ref=\"ref-1\",\n created_by=\"operator\",\n status=CandidateStatus.DRAFT,\n )\n db_session.add(candidate)\n db_session.commit()\n\n # Valid transition: DRAFT -> PREPARED\n candidate.transition_to(CandidateStatus.PREPARED)\n assert candidate.status == CandidateStatus.PREPARED\n\n # Invalid transition: PREPARED -> DRAFT (should raise IllegalTransitionError)\n from src.services.clean_release.exceptions import IllegalTransitionError\n\n with pytest.raises(IllegalTransitionError, match=\"Forbidden transition\"):\n candidate.transition_to(CandidateStatus.DRAFT)\n\n\n# #endregion test_candidate_lifecycle_transitions\n\n\n# #region test_manifest_versioning_and_immutability [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify manifest versions increment monotonically and older snapshots remain queryable.\ndef test_manifest_versioning_and_immutability(db_session):\n \"\"\"\n @PURPOSE: Verify manifest versioning and immutability invariants.\n \"\"\"\n candidate_id = \"test-candidate-2\"\n\n # Create version 1\n m1 = DistributionManifest(\n id=\"manifest-v1\",\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"hash1\",\n artifacts_digest=\"hash1\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m1)\n\n # Create version 2\n m2 = DistributionManifest(\n id=\"manifest-v2\",\n candidate_id=candidate_id,\n manifest_version=2,\n manifest_digest=\"hash2\",\n artifacts_digest=\"hash2\",\n source_snapshot_ref=\"ref1\",\n content_json={},\n created_at=datetime.now(UTC),\n created_by=\"operator\",\n )\n db_session.add(m2)\n db_session.commit()\n\n latest = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .order_by(DistributionManifest.manifest_version.desc())\n .first()\n )\n assert latest.manifest_version == 2\n assert latest.id == \"manifest-v2\"\n\n all_manifests = (\n db_session.query(DistributionManifest)\n .filter_by(candidate_id=candidate_id)\n .all()\n )\n assert len(all_manifests) == 2\n\n\n# #endregion test_manifest_versioning_and_immutability\n\n\n# #region _valid_artifacts [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Provide canonical valid artifact payload used by candidate registration tests.\ndef _valid_artifacts():\n return [\n {\n \"id\": \"art-1\",\n \"path\": \"bin/app\",\n \"sha256\": \"abc123\",\n \"size\": 42,\n }\n ]\n\n\n# #endregion _valid_artifacts\n\n\n# #region test_register_candidate_rejects_duplicate_candidate_id [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify duplicate candidate_id registration is rejected by service invariants.\ndef test_register_candidate_rejects_duplicate_candidate_id():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n with pytest.raises(ValueError, match=\"already exists\"):\n register_candidate(\n repository=repository,\n candidate_id=\"dup-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha1\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n\n# #endregion test_register_candidate_rejects_duplicate_candidate_id\n\n\n# #region test_register_candidate_rejects_malformed_artifact_input [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify candidate registration rejects artifact payloads missing required fields.\ndef test_register_candidate_rejects_malformed_artifact_input():\n repository = CleanReleaseRepository()\n bad_artifacts = [{\"id\": \"art-1\", \"path\": \"bin/app\", \"size\": 42}] # missing sha256\n\n with pytest.raises(ValueError, match=\"missing required field 'sha256'\"):\n register_candidate(\n repository=repository,\n candidate_id=\"bad-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha2\",\n created_by=\"operator\",\n artifacts=bad_artifacts,\n )\n\n\n# #endregion test_register_candidate_rejects_malformed_artifact_input\n\n\n# #region test_register_candidate_rejects_empty_artifact_set [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify candidate registration rejects empty artifact collections.\ndef test_register_candidate_rejects_empty_artifact_set():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"artifacts must not be empty\"):\n register_candidate(\n repository=repository,\n candidate_id=\"empty-art-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha3\",\n created_by=\"operator\",\n artifacts=[],\n )\n\n\n# #endregion test_register_candidate_rejects_empty_artifact_set\n\n\n# #region test_manifest_service_rebuild_creates_new_version [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify repeated manifest build creates a new incremented immutable version.\ndef test_manifest_service_rebuild_creates_new_version():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-version-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha10\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n first = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n second = build_manifest_snapshot(\n repository=repository, candidate_id=\"manifest-version-1\", created_by=\"operator\"\n )\n\n assert first.manifest_version == 1\n assert second.manifest_version == 2\n assert first.id != second.id\n\n\n# #endregion test_manifest_service_rebuild_creates_new_version\n\n\n# #region test_manifest_service_existing_manifest_cannot_be_mutated [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify existing manifest snapshot remains immutable when rebuilding newer manifest version.\ndef test_manifest_service_existing_manifest_cannot_be_mutated():\n repository = CleanReleaseRepository()\n register_candidate(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha11\",\n created_by=\"operator\",\n artifacts=_valid_artifacts(),\n )\n\n created = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n original_digest = created.manifest_digest\n\n rebuilt = build_manifest_snapshot(\n repository=repository,\n candidate_id=\"manifest-immutable-1\",\n created_by=\"operator\",\n )\n old_manifest = repository.get_manifest(created.id)\n\n assert old_manifest is not None\n assert old_manifest.manifest_digest == original_digest\n assert old_manifest.id == created.id\n assert rebuilt.id != created.id\n\n\n# #endregion test_manifest_service_existing_manifest_cannot_be_mutated\n\n\n# #region test_manifest_service_rejects_missing_candidate [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [test_candidate_manifest_services]\n# @PURPOSE: Verify manifest build fails with missing candidate identifier.\ndef test_manifest_service_rejects_missing_candidate():\n repository = CleanReleaseRepository()\n\n with pytest.raises(ValueError, match=\"not found\"):\n build_manifest_snapshot(\n repository=repository,\n candidate_id=\"missing-candidate\",\n created_by=\"operator\",\n )\n\n\n# #endregion test_manifest_service_rejects_missing_candidate\n# #endregion test_candidate_manifest_services\n" }, { "contract_id": "test_candidate_lifecycle_transitions", @@ -51112,7 +51992,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", "start_line": 1, - "end_line": 175, + "end_line": 174, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51139,14 +52019,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestComplianceExecutionService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization\n# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.\n# @LAYER Tests\n# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_with_candidate_policy_registry [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# #endregion _seed_with_candidate_policy_registry\n\n\n# #region test_run_without_manifest_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.\n# @PRE: Candidate/policy exist but manifest is missing.\n# @POST: start_check_run raises ValueError and no run is persisted.\ndef test_run_without_manifest_rejected():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=False)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n with pytest.raises(ValueError, match=\"Manifest or Policy not found\"):\n orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n assert len(repository.check_runs) == 0\n# #endregion test_run_without_manifest_rejected\n\n\n# #region test_task_crash_mid_run_marks_failed [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure execution crash conditions force FAILED run status.\n# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.\n# @POST: execute_stages persists run with FAILED status.\ndef test_task_crash_mid_run_marks_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=True)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n # Simulate mid-run crash dependency loss: registry snapshot disappears.\n repository.registries.clear()\n\n failed = orchestrator.execute_stages(run)\n assert failed.status == RunStatus.FAILED\n# #endregion test_task_crash_mid_run_marks_failed\n\n\n# #region test_blocked_run_finalization_blocks_report_builder [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure blocked runs require blocking violations before report creation.\n# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.\n# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.\ndef test_blocked_run_finalization_blocks_report_builder():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(\n with_manifest=True,\n prohibited_detected_count=1,\n )\n orchestrator = CleanComplianceOrchestrator(repository)\n builder = ComplianceReportBuilder(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == ComplianceDecision.BLOCKED\n assert run.status == RunStatus.SUCCEEDED\n\n with pytest.raises(ValueError, match=\"Blocked run requires at least one blocking violation\"):\n builder.build_report_payload(run, [])\n# #endregion test_blocked_run_finalization_blocks_report_builder\n\n# #endregion TestComplianceExecutionService\n" + "body": "# #region TestComplianceExecutionService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization\n# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.\n# @LAYER Tests\n# @INVARIANT: Missing manifest prevents run startup; failed execution cannot finalize as PASSED.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport pytest\n\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n ComplianceDecision,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import CleanComplianceOrchestrator\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_with_candidate_policy_registry [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Build deterministic repository state for run startup tests.\n# @PRE: candidate_id and snapshot ids are non-empty.\n# @POST: Returns repository with candidate, policy and registry; manifest is optional.\ndef _seed_with_candidate_policy_registry(\n *,\n with_manifest: bool,\n prohibited_detected_count: int = 0,\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-us2-1\"\n policy_id = \"policy-us2-1\"\n registry_id = \"registry-us2-1\"\n manifest_id = \"manifest-us2-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-us2\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=registry_id,\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=registry_id,\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-us2-1\",\n artifacts_digest=\"digest-us2-1\",\n source_snapshot_ref=\"git:sha-us2\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0 if prohibited_detected_count == 0 else prohibited_detected_count,\n \"prohibited_detected_count\": prohibited_detected_count,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n# #endregion _seed_with_candidate_policy_registry\n\n\n# #region test_run_without_manifest_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure compliance run cannot start when manifest is unresolved.\n# @PRE: Candidate/policy exist but manifest is missing.\n# @POST: start_check_run raises ValueError and no run is persisted.\ndef test_run_without_manifest_rejected():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=False)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n with pytest.raises(ValueError, match=\"Manifest or Policy not found\"):\n orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n assert len(repository.check_runs) == 0\n# #endregion test_run_without_manifest_rejected\n\n\n# #region test_task_crash_mid_run_marks_failed [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure execution crash conditions force FAILED run status.\n# @PRE: Run exists, then required dependency becomes unavailable before execute_stages.\n# @POST: execute_stages persists run with FAILED status.\ndef test_task_crash_mid_run_marks_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(with_manifest=True)\n orchestrator = CleanComplianceOrchestrator(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n\n # Simulate mid-run crash dependency loss: registry snapshot disappears.\n repository.registries.clear()\n\n failed = orchestrator.execute_stages(run)\n assert failed.status == RunStatus.FAILED\n# #endregion test_task_crash_mid_run_marks_failed\n\n\n# #region test_blocked_run_finalization_blocks_report_builder [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceExecutionService\n# @PURPOSE: Ensure blocked runs require blocking violations before report creation.\n# @PRE: Manifest contains prohibited artifacts leading to BLOCKED decision.\n# @POST: finalize keeps BLOCKED and report_builder rejects zero blocking violations.\ndef test_blocked_run_finalization_blocks_report_builder():\n repository, candidate_id, policy_id, manifest_id = _seed_with_candidate_policy_registry(\n with_manifest=True,\n prohibited_detected_count=1,\n )\n orchestrator = CleanComplianceOrchestrator(repository)\n builder = ComplianceReportBuilder(repository)\n\n run = orchestrator.start_check_run(\n candidate_id=candidate_id,\n policy_id=policy_id,\n requested_by=\"tester\",\n manifest_id=manifest_id,\n )\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n assert run.final_status == ComplianceDecision.BLOCKED\n assert run.status == RunStatus.SUCCEEDED\n\n with pytest.raises(ValueError, match=\"Blocked run requires at least one blocking violation\"):\n builder.build_report_payload(run, [])\n# #endregion test_blocked_run_finalization_blocks_report_builder\n\n# #endregion TestComplianceExecutionService\n" }, { "contract_id": "_seed_with_candidate_policy_registry", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 27, - "end_line": 98, + "start_line": 26, + "end_line": 97, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51172,8 +52052,8 @@ "contract_id": "test_run_without_manifest_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 101, - "end_line": 119, + "start_line": 100, + "end_line": 118, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51199,8 +52079,8 @@ "contract_id": "test_task_crash_mid_run_marks_failed", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 122, - "end_line": 143, + "start_line": 121, + "end_line": 142, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51226,8 +52106,8 @@ "contract_id": "test_blocked_run_finalization_blocks_report_builder", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_execution_service.py", - "start_line": 146, - "end_line": 173, + "start_line": 145, + "end_line": 172, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51254,7 +52134,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", "start_line": 1, - "end_line": 307, + "end_line": 306, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51281,14 +52161,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestComplianceTaskIntegration [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, task-manager, integration\n# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.\n# @LAYER Tests\n# @INVARIANT: Compliance execution triggered as task produces terminal task status and persists run evidence.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import UTC, datetime\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom src.core.task_manager.manager import TaskManager\nfrom src.core.task_manager.models import TaskStatus\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_repository [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# #endregion _seed_repository\n\n\n# #region CleanReleaseCompliancePlugin [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# #endregion CleanReleaseCompliancePlugin\n\n\n# #region _PluginLoaderStub [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# #endregion _PluginLoaderStub\n\n\n# #region _make_task_manager [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Build TaskManager with mocked persistence services for isolated integration tests.\n# @POST: Returns TaskManager ready for async task execution.\ndef _make_task_manager() -> TaskManager:\n plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())\n\n with (\n patch(\n \"src.core.task_manager.manager.TaskPersistenceService\"\n ) as mock_persistence,\n patch(\n \"src.core.task_manager.manager.TaskLogPersistenceService\"\n ) as mock_log_persistence,\n ):\n mock_persistence.return_value.load_tasks.return_value = []\n mock_persistence.return_value.persist_task = MagicMock()\n mock_log_persistence.return_value.add_logs = MagicMock()\n mock_log_persistence.return_value.get_logs = MagicMock(return_value=[])\n mock_log_persistence.return_value.get_log_stats = MagicMock()\n mock_log_persistence.return_value.get_sources = MagicMock(return_value=[])\n\n return TaskManager(plugin_loader)\n\n\n# #endregion _make_task_manager\n\n\n# #region _wait_for_terminal_task [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Poll task registry until target task reaches terminal status.\n# @PRE: task_id exists in manager registry.\n# @POST: Returns task with SUCCESS or FAILED status, otherwise raises TimeoutError.\nasync def _wait_for_terminal_task(\n manager: TaskManager, task_id: str, timeout_seconds: float = 3.0\n):\n started = asyncio.get_running_loop().time()\n while True:\n task = manager.get_task(task_id)\n if task and task.status in {TaskStatus.SUCCESS, TaskStatus.FAILED}:\n return task\n if asyncio.get_running_loop().time() - started > timeout_seconds:\n raise TimeoutError(f\"Task {task_id} did not reach terminal status\")\n await asyncio.sleep(0.05)\n\n\n# #endregion _wait_for_terminal_task\n\n\n# #region test_compliance_run_executes_as_task_manager_task [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify successful compliance execution is observable as TaskManager SUCCESS task.\n# @PRE: Candidate, policy and manifest are available in repository.\n# @POST: Task ends with SUCCESS; run is persisted with SUCCEEDED status and task binding.\n@pytest.mark.asyncio\nasync def test_compliance_run_executes_as_task_manager_task():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=True\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.SUCCESS\n assert isinstance(finished.result, dict)\n\n run_id = finished.result[\"run_id\"]\n run = repository.get_check_run(run_id)\n assert run is not None\n assert run.status == RunStatus.SUCCEEDED\n assert run.task_id == task.id\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# #endregion test_compliance_run_executes_as_task_manager_task\n\n\n# #region test_compliance_run_missing_manifest_marks_task_failed [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify missing manifest startup failure is surfaced as TaskManager FAILED task.\n# @PRE: Candidate/policy exist but manifest is absent.\n# @POST: Task ends with FAILED and run history remains empty.\n@pytest.mark.asyncio\nasync def test_compliance_run_missing_manifest_marks_task_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=False\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.FAILED\n assert len(repository.check_runs) == 0\n assert any(\n \"Manifest or Policy not found\" in log.message for log in finished.logs\n )\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# #endregion test_compliance_run_missing_manifest_marks_task_failed\n\n# #endregion TestComplianceTaskIntegration\n" + "body": "# #region TestComplianceTaskIntegration [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, compliance, task-manager, integration\n# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.\n# @LAYER Tests\n# @INVARIANT: Compliance execution triggered as task produces terminal task status and persists run evidence.\n\nfrom __future__ import annotations\n\nimport asyncio\nfrom datetime import UTC, datetime\nimport pytest\nfrom typing import Any\nfrom unittest.mock import MagicMock, patch\n\nfrom src.core.task_manager.manager import TaskManager\nfrom src.core.task_manager.models import TaskStatus\nfrom src.models.clean_release import (\n CleanPolicySnapshot,\n DistributionManifest,\n ReleaseCandidate,\n SourceRegistrySnapshot,\n)\nfrom src.services.clean_release.compliance_orchestrator import (\n CleanComplianceOrchestrator,\n)\nfrom src.services.clean_release.enums import CandidateStatus, RunStatus\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_repository [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Prepare deterministic candidate/policy/registry/manifest fixtures for task integration tests.\n# @PRE: with_manifest controls manifest availability.\n# @POST: Returns initialized repository and identifiers for compliance run startup.\ndef _seed_repository(\n *, with_manifest: bool\n) -> tuple[CleanReleaseRepository, str, str, str]:\n repository = CleanReleaseRepository()\n candidate_id = \"cand-task-int-1\"\n policy_id = \"policy-task-int-1\"\n manifest_id = \"manifest-task-int-1\"\n\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-task-int\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=CandidateStatus.MANIFEST_BUILT.value,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-task-int-1\",\n registry_id=\"trusted-registry\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"repo.internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n repository.save_policy(\n CleanPolicySnapshot(\n id=policy_id,\n policy_id=\"trusted-policy\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-task-int-1\",\n immutable=True,\n )\n )\n\n if with_manifest:\n repository.save_manifest(\n DistributionManifest(\n id=manifest_id,\n candidate_id=candidate_id,\n manifest_version=1,\n manifest_digest=\"digest-task-int\",\n artifacts_digest=\"digest-task-int\",\n source_snapshot_ref=\"git:sha-task-int\",\n content_json={\n \"summary\": {\n \"included_count\": 1,\n \"excluded_count\": 0,\n \"prohibited_detected_count\": 0,\n }\n },\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n immutable=True,\n )\n )\n\n return repository, candidate_id, policy_id, manifest_id\n\n\n# #endregion _seed_repository\n\n\n# #region CleanReleaseCompliancePlugin [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: TaskManager plugin shim that executes clean release compliance orchestration.\nclass CleanReleaseCompliancePlugin:\n @property\n def id(self) -> str:\n return \"clean-release-compliance\"\n\n @property\n def name(self) -> str:\n return \"clean_release_compliance\"\n\n def execute(self, params: dict[str, Any], context=None):\n orchestrator = CleanComplianceOrchestrator(params[\"repository\"])\n run = orchestrator.start_check_run(\n candidate_id=params[\"candidate_id\"],\n policy_id=params[\"policy_id\"],\n requested_by=params.get(\"requested_by\", \"tester\"),\n manifest_id=params[\"manifest_id\"],\n )\n run.task_id = params[\"_task_id\"]\n params[\"repository\"].save_check_run(run)\n\n run = orchestrator.execute_stages(run)\n run = orchestrator.finalize_run(run)\n\n if context is not None:\n context.logger.info(\"Compliance run completed via TaskManager plugin\")\n\n return {\n \"run_id\": run.id,\n \"run_status\": run.status,\n \"final_status\": run.final_status,\n }\n\n\n# #endregion CleanReleaseCompliancePlugin\n\n\n# #region _PluginLoaderStub [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.\n# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.\nclass _PluginLoaderStub:\n # @CONTRACT: Partial PluginLoader stub. Implements: has_plugin, get_plugin. Stubs (NotImplementedError): list_plugins, get_all_plugins, get_all_plugin_configs.\n def __init__(self, plugin: CleanReleaseCompliancePlugin):\n self._plugin = plugin\n\n def has_plugin(self, plugin_id: str) -> bool:\n return plugin_id == self._plugin.id\n\n def get_plugin(self, plugin_id: str):\n if plugin_id != self._plugin.id:\n raise ValueError(\"Plugin not found\")\n return self._plugin\n\n def list_plugins(self):\n raise NotImplementedError(\n \"list_plugins not implemented in _PluginLoaderStub; add if test path requires plugin enumeration\"\n )\n\n def get_all_plugins(self):\n raise NotImplementedError(\n \"get_all_plugins not implemented in _PluginLoaderStub; add if test path requires full plugin set\"\n )\n\n def get_all_plugin_configs(self):\n raise NotImplementedError(\n \"get_all_plugin_configs not implemented in _PluginLoaderStub; add if test path requires plugin configs\"\n )\n\n\n# #endregion _PluginLoaderStub\n\n\n# #region _make_task_manager [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Build TaskManager with mocked persistence services for isolated integration tests.\n# @POST: Returns TaskManager ready for async task execution.\ndef _make_task_manager() -> TaskManager:\n plugin_loader = _PluginLoaderStub(CleanReleaseCompliancePlugin())\n\n with (\n patch(\n \"src.core.task_manager.manager.TaskPersistenceService\"\n ) as mock_persistence,\n patch(\n \"src.core.task_manager.manager.TaskLogPersistenceService\"\n ) as mock_log_persistence,\n ):\n mock_persistence.return_value.load_tasks.return_value = []\n mock_persistence.return_value.persist_task = MagicMock()\n mock_log_persistence.return_value.add_logs = MagicMock()\n mock_log_persistence.return_value.get_logs = MagicMock(return_value=[])\n mock_log_persistence.return_value.get_log_stats = MagicMock()\n mock_log_persistence.return_value.get_sources = MagicMock(return_value=[])\n\n return TaskManager(plugin_loader)\n\n\n# #endregion _make_task_manager\n\n\n# #region _wait_for_terminal_task [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Poll task registry until target task reaches terminal status.\n# @PRE: task_id exists in manager registry.\n# @POST: Returns task with SUCCESS or FAILED status, otherwise raises TimeoutError.\nasync def _wait_for_terminal_task(\n manager: TaskManager, task_id: str, timeout_seconds: float = 3.0\n):\n started = asyncio.get_running_loop().time()\n while True:\n task = manager.get_task(task_id)\n if task and task.status in {TaskStatus.SUCCESS, TaskStatus.FAILED}:\n return task\n if asyncio.get_running_loop().time() - started > timeout_seconds:\n raise TimeoutError(f\"Task {task_id} did not reach terminal status\")\n await asyncio.sleep(0.05)\n\n\n# #endregion _wait_for_terminal_task\n\n\n# #region test_compliance_run_executes_as_task_manager_task [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify successful compliance execution is observable as TaskManager SUCCESS task.\n# @PRE: Candidate, policy and manifest are available in repository.\n# @POST: Task ends with SUCCESS; run is persisted with SUCCEEDED status and task binding.\n@pytest.mark.asyncio\nasync def test_compliance_run_executes_as_task_manager_task():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=True\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.SUCCESS\n assert isinstance(finished.result, dict)\n\n run_id = finished.result[\"run_id\"]\n run = repository.get_check_run(run_id)\n assert run is not None\n assert run.status == RunStatus.SUCCEEDED\n assert run.task_id == task.id\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# #endregion test_compliance_run_executes_as_task_manager_task\n\n\n# #region test_compliance_run_missing_manifest_marks_task_failed [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestComplianceTaskIntegration\n# @PURPOSE: Verify missing manifest startup failure is surfaced as TaskManager FAILED task.\n# @PRE: Candidate/policy exist but manifest is absent.\n# @POST: Task ends with FAILED and run history remains empty.\n@pytest.mark.asyncio\nasync def test_compliance_run_missing_manifest_marks_task_failed():\n repository, candidate_id, policy_id, manifest_id = _seed_repository(\n with_manifest=False\n )\n manager = _make_task_manager()\n\n try:\n task = await manager.create_task(\n \"clean-release-compliance\",\n {\n \"repository\": repository,\n \"candidate_id\": candidate_id,\n \"policy_id\": policy_id,\n \"manifest_id\": manifest_id,\n \"requested_by\": \"integration-tester\",\n },\n )\n finished = await _wait_for_terminal_task(manager, task.id)\n\n assert finished.status == TaskStatus.FAILED\n assert len(repository.check_runs) == 0\n assert any(\n \"Manifest or Policy not found\" in log.message for log in finished.logs\n )\n finally:\n manager._flusher_stop_event.set()\n manager._flusher_thread.join(timeout=2)\n\n\n# #endregion test_compliance_run_missing_manifest_marks_task_failed\n\n# #endregion TestComplianceTaskIntegration\n" }, { "contract_id": "_seed_repository", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 32, - "end_line": 102, + "start_line": 31, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51314,8 +52194,8 @@ "contract_id": "CleanReleaseCompliancePlugin", "contract_type": "Class", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 105, - "end_line": 141, + "start_line": 104, + "end_line": 140, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51339,8 +52219,8 @@ "contract_id": "_PluginLoaderStub", "contract_type": "Class", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 144, - "end_line": 177, + "start_line": 143, + "end_line": 176, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51366,8 +52246,8 @@ "contract_id": "_make_task_manager", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 180, - "end_line": 205, + "start_line": 179, + "end_line": 204, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51392,8 +52272,8 @@ "contract_id": "_wait_for_terminal_task", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 208, - "end_line": 226, + "start_line": 207, + "end_line": 225, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51419,8 +52299,8 @@ "contract_id": "test_compliance_run_executes_as_task_manager_task", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 229, - "end_line": 267, + "start_line": 228, + "end_line": 266, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51446,8 +52326,8 @@ "contract_id": "test_compliance_run_missing_manifest_marks_task_failed", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_compliance_task_integration.py", - "start_line": 270, - "end_line": 305, + "start_line": 269, + "end_line": 304, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51588,7 +52468,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", "start_line": 1, - "end_line": 121, + "end_line": 120, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51626,14 +52506,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestPolicyResolutionService [C:2] [TYPE Module]\n# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts\n# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.\n# @LAYER Tests\n# @RELATION DEPENDS_ON -> [EXT:frontend:policy_resolution_service]\n# @RELATION DEPENDS_ON -> [EXT:frontend:repository]\n# @RELATION DEPENDS_ON -> [clean_release_exceptions]\n# @INVARIANT: Resolution uses only ConfigManager active IDs and rejects runtime override attempts.\n\nfrom __future__ import annotations\n\nfrom types import SimpleNamespace\n\nimport pytest\n\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.services.clean_release.exceptions import PolicyResolutionError\nfrom src.services.clean_release.policy_resolution_service import (\n resolve_trusted_policy_snapshots,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _config_manager [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# #endregion _config_manager\n\n\n# #region test_resolve_trusted_policy_snapshots_missing_profile [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted profile is not configured.\n# @PRE: active_policy_id is None.\n# @POST: Raises PolicyResolutionError with missing trusted profile reason.\ndef test_resolve_trusted_policy_snapshots_missing_profile():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=None, registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted profile\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_missing_profile\n\n\n# #region test_resolve_trusted_policy_snapshots_missing_registry [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted registry is not configured.\n# @PRE: active_registry_id is None and active_policy_id is set.\n# @POST: Raises PolicyResolutionError with missing trusted registry reason.\ndef test_resolve_trusted_policy_snapshots_missing_registry():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=None)\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted registry\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_missing_registry\n\n\n# #region test_resolve_trusted_policy_snapshots_rejects_override_attempt [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure runtime override attempt is rejected even if snapshots exist.\n# @PRE: valid trusted snapshots exist in repository and override is provided.\n# @POST: Raises PolicyResolutionError with override forbidden reason.\ndef test_resolve_trusted_policy_snapshots_rejects_override_attempt():\n repository = CleanReleaseRepository()\n repository.save_policy(\n CleanPolicySnapshot(\n id=\"policy-1\",\n policy_id=\"baseline\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-1\",\n immutable=True,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-1\",\n registry_id=\"trusted\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"override attempt is forbidden\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n policy_id_override=\"policy-override\",\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_rejects_override_attempt\n\n# #endregion TestPolicyResolutionService\n" + "body": "# #region TestPolicyResolutionService [C:2] [TYPE Module]\n# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts\n# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.\n# @LAYER Tests\n# @RELATION DEPENDS_ON -> [EXT:frontend:policy_resolution_service]\n# @RELATION DEPENDS_ON -> [EXT:frontend:repository]\n# @RELATION DEPENDS_ON -> [clean_release_exceptions]\n# @INVARIANT: Resolution uses only ConfigManager active IDs and rejects runtime override attempts.\n\nfrom __future__ import annotations\n\nimport pytest\nfrom types import SimpleNamespace\n\nfrom src.models.clean_release import CleanPolicySnapshot, SourceRegistrySnapshot\nfrom src.services.clean_release.exceptions import PolicyResolutionError\nfrom src.services.clean_release.policy_resolution_service import (\n resolve_trusted_policy_snapshots,\n)\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _config_manager [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Build deterministic ConfigManager-like stub for tests.\n# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.\n# @PRE: policy_id and registry_id may be None or non-empty strings.\n# @POST: Returns object exposing get_config().settings.clean_release active IDs.\ndef _config_manager(policy_id, registry_id):\n clean_release = SimpleNamespace(\n active_policy_id=policy_id, active_registry_id=registry_id\n )\n settings = SimpleNamespace(clean_release=clean_release)\n config = SimpleNamespace(settings=settings)\n return SimpleNamespace(get_config=lambda: config)\n\n\n# #endregion _config_manager\n\n\n# #region test_resolve_trusted_policy_snapshots_missing_profile [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted profile is not configured.\n# @PRE: active_policy_id is None.\n# @POST: Raises PolicyResolutionError with missing trusted profile reason.\ndef test_resolve_trusted_policy_snapshots_missing_profile():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=None, registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted profile\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_missing_profile\n\n\n# #region test_resolve_trusted_policy_snapshots_missing_registry [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure resolution fails when trusted registry is not configured.\n# @PRE: active_registry_id is None and active_policy_id is set.\n# @POST: Raises PolicyResolutionError with missing trusted registry reason.\ndef test_resolve_trusted_policy_snapshots_missing_registry():\n repository = CleanReleaseRepository()\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=None)\n\n with pytest.raises(PolicyResolutionError, match=\"missing trusted registry\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_missing_registry\n\n\n# #region test_resolve_trusted_policy_snapshots_rejects_override_attempt [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> [EXT:frontend:TestPolicyResolutionService]\n# @PURPOSE: Ensure runtime override attempt is rejected even if snapshots exist.\n# @PRE: valid trusted snapshots exist in repository and override is provided.\n# @POST: Raises PolicyResolutionError with override forbidden reason.\ndef test_resolve_trusted_policy_snapshots_rejects_override_attempt():\n repository = CleanReleaseRepository()\n repository.save_policy(\n CleanPolicySnapshot(\n id=\"policy-1\",\n policy_id=\"baseline\",\n policy_version=\"1.0.0\",\n content_json={\"rules\": []},\n registry_snapshot_id=\"registry-1\",\n immutable=True,\n )\n )\n repository.save_registry(\n SourceRegistrySnapshot(\n id=\"registry-1\",\n registry_id=\"trusted\",\n registry_version=\"1.0.0\",\n allowed_hosts=[\"internal.local\"],\n allowed_schemes=[\"https\"],\n allowed_source_types=[\"repo\"],\n immutable=True,\n )\n )\n\n config_manager = _config_manager(policy_id=\"policy-1\", registry_id=\"registry-1\")\n\n with pytest.raises(PolicyResolutionError, match=\"override attempt is forbidden\"):\n resolve_trusted_policy_snapshots(\n config_manager=config_manager,\n repository=repository,\n policy_id_override=\"policy-override\",\n )\n\n\n# #endregion test_resolve_trusted_policy_snapshots_rejects_override_attempt\n\n# #endregion TestPolicyResolutionService\n" }, { "contract_id": "_config_manager", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 24, - "end_line": 39, + "start_line": 23, + "end_line": 38, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51660,8 +52540,8 @@ "contract_id": "test_resolve_trusted_policy_snapshots_missing_profile", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 42, - "end_line": 58, + "start_line": 41, + "end_line": 57, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51687,8 +52567,8 @@ "contract_id": "test_resolve_trusted_policy_snapshots_missing_registry", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 61, - "end_line": 77, + "start_line": 60, + "end_line": 76, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51714,8 +52594,8 @@ "contract_id": "test_resolve_trusted_policy_snapshots_rejects_override_attempt", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_policy_resolution_service.py", - "start_line": 80, - "end_line": 119, + "start_line": 79, + "end_line": 118, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51742,7 +52622,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_publication_service.py", "start_line": 1, - "end_line": 149, + "end_line": 148, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51769,14 +52649,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestPublicationService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, publication, revoke, gate\n# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.\n# @LAYER Tests\n# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\n\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, PublicationStatus\nfrom src.services.clean_release.exceptions import PublicationGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_candidate_with_passed_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# #endregion _seed_candidate_with_passed_report\n\n\n# #region test_publish_without_approval_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure publish action is blocked until candidate is approved.\n# @PRE: Candidate has PASSED report but status is not APPROVED.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_publish_without_approval_rejected():\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-1\",\n )\n# #endregion test_publish_without_approval_rejected\n\n\n# #region test_revoke_unknown_publication_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure revocation is rejected for unknown publication id.\n# @PRE: Repository has no matching publication record.\n# @POST: revoke_publication raises PublicationGateError.\ndef test_revoke_unknown_publication_rejected():\n from src.services.clean_release.publication_service import revoke_publication\n\n repository, _, _ = _seed_candidate_with_passed_report()\n\n with pytest.raises(PublicationGateError, match=\"not found\"):\n revoke_publication(\n repository=repository,\n publication_id=\"missing-publication\",\n revoked_by=\"publisher\",\n comment=\"unknown publication id\",\n )\n# #endregion test_revoke_unknown_publication_rejected\n\n\n# #region test_republish_after_revoke_creates_new_active_record [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.\n# @PRE: Candidate is APPROVED and first publication has been revoked.\n# @POST: New publish call returns distinct publication id with ACTIVE status.\ndef test_republish_after_revoke_creates_new_active_record():\n from src.services.clean_release.approval_service import approve_candidate\n from src.services.clean_release.publication_service import publish_candidate, revoke_publication\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"approval before publication\",\n )\n\n first = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-1\",\n )\n revoked = revoke_publication(\n repository=repository,\n publication_id=first.id,\n revoked_by=\"publisher\",\n comment=\"rollback\",\n )\n second = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-2\",\n )\n\n assert first.id != second.id\n assert revoked.status == PublicationStatus.REVOKED.value\n assert second.status == PublicationStatus.ACTIVE.value\n# #endregion test_republish_after_revoke_creates_new_active_record\n\n# #endregion TestPublicationService\n" + "body": "# #region TestPublicationService [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, publication, revoke, gate\n# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.\n# @LAYER Tests\n# @INVARIANT: Publish requires approval; revoke requires existing publication; republish after revoke is allowed as a new record.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport pytest\n\nfrom src.models.clean_release import ComplianceReport, ReleaseCandidate\nfrom src.services.clean_release.enums import CandidateStatus, ComplianceDecision, PublicationStatus\nfrom src.services.clean_release.exceptions import PublicationGateError\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _seed_candidate_with_passed_report [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Seed candidate/report fixtures for publication gate scenarios.\n# @PRE: candidate_id and report_id are non-empty.\n# @POST: Repository contains candidate and PASSED report.\ndef _seed_candidate_with_passed_report(\n *,\n candidate_id: str = \"cand-publish-1\",\n report_id: str = \"CCR-publish-1\",\n candidate_status: CandidateStatus = CandidateStatus.CHECK_PASSED,\n) -> tuple[CleanReleaseRepository, str, str]:\n repository = CleanReleaseRepository()\n repository.save_candidate(\n ReleaseCandidate(\n id=candidate_id,\n version=\"1.0.0\",\n source_snapshot_ref=\"git:sha-publish-1\",\n created_by=\"tester\",\n created_at=datetime.now(UTC),\n status=candidate_status.value,\n )\n )\n repository.save_report(\n ComplianceReport(\n id=report_id,\n run_id=\"run-publish-1\",\n candidate_id=candidate_id,\n final_status=ComplianceDecision.PASSED.value,\n summary_json={\"operator_summary\": \"seed\", \"violations_count\": 0, \"blocking_violations_count\": 0},\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n )\n return repository, candidate_id, report_id\n# #endregion _seed_candidate_with_passed_report\n\n\n# #region test_publish_without_approval_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure publish action is blocked until candidate is approved.\n# @PRE: Candidate has PASSED report but status is not APPROVED.\n# @POST: publish_candidate raises PublicationGateError.\ndef test_publish_without_approval_rejected():\n from src.services.clean_release.publication_service import publish_candidate\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n\n with pytest.raises(PublicationGateError, match=\"APPROVED\"):\n publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"rel-1\",\n )\n# #endregion test_publish_without_approval_rejected\n\n\n# #region test_revoke_unknown_publication_rejected [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure revocation is rejected for unknown publication id.\n# @PRE: Repository has no matching publication record.\n# @POST: revoke_publication raises PublicationGateError.\ndef test_revoke_unknown_publication_rejected():\n from src.services.clean_release.publication_service import revoke_publication\n\n repository, _, _ = _seed_candidate_with_passed_report()\n\n with pytest.raises(PublicationGateError, match=\"not found\"):\n revoke_publication(\n repository=repository,\n publication_id=\"missing-publication\",\n revoked_by=\"publisher\",\n comment=\"unknown publication id\",\n )\n# #endregion test_revoke_unknown_publication_rejected\n\n\n# #region test_republish_after_revoke_creates_new_active_record [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestPublicationService\n# @PURPOSE: Ensure republish after revoke is allowed and creates a new ACTIVE record.\n# @PRE: Candidate is APPROVED and first publication has been revoked.\n# @POST: New publish call returns distinct publication id with ACTIVE status.\ndef test_republish_after_revoke_creates_new_active_record():\n from src.services.clean_release.approval_service import approve_candidate\n from src.services.clean_release.publication_service import publish_candidate, revoke_publication\n\n repository, candidate_id, report_id = _seed_candidate_with_passed_report(\n candidate_status=CandidateStatus.CHECK_PASSED,\n )\n approve_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n decided_by=\"approver\",\n comment=\"approval before publication\",\n )\n\n first = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-1\",\n )\n revoked = revoke_publication(\n repository=repository,\n publication_id=first.id,\n revoked_by=\"publisher\",\n comment=\"rollback\",\n )\n second = publish_candidate(\n repository=repository,\n candidate_id=candidate_id,\n report_id=report_id,\n published_by=\"publisher\",\n target_channel=\"stable\",\n publication_ref=\"release-2\",\n )\n\n assert first.id != second.id\n assert revoked.status == PublicationStatus.REVOKED.value\n assert second.status == PublicationStatus.ACTIVE.value\n# #endregion test_republish_after_revoke_creates_new_active_record\n\n# #endregion TestPublicationService\n" }, { "contract_id": "_seed_candidate_with_passed_report", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 20, - "end_line": 54, + "start_line": 19, + "end_line": 53, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51802,8 +52682,8 @@ "contract_id": "test_publish_without_approval_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 57, - "end_line": 78, + "start_line": 56, + "end_line": 77, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51829,8 +52709,8 @@ "contract_id": "test_revoke_unknown_publication_rejected", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 81, - "end_line": 98, + "start_line": 80, + "end_line": 97, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51856,8 +52736,8 @@ "contract_id": "test_republish_after_revoke_creates_new_active_record", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_publication_service.py", - "start_line": 101, - "end_line": 147, + "start_line": 100, + "end_line": 146, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51884,7 +52764,7 @@ "contract_type": "Module", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", "start_line": 1, - "end_line": 146, + "end_line": 145, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51912,14 +52792,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestReportAuditImmutability [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only\n# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.\n# @LAYER Tests\n# @INVARIANT: Built reports are immutable snapshots; audit hooks produce append-only event traces.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom src.models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n)\nfrom src.services.clean_release.audit_service import (\n audit_check_run,\n audit_preparation,\n audit_report,\n)\nfrom src.services.clean_release.enums import ComplianceDecision, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _terminal_run [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Build deterministic terminal run fixture for report snapshot tests.\n# @PRE: final_status is a valid ComplianceDecision value.\n# @POST: Returns a terminal ComplianceRun suitable for report generation.\ndef _terminal_run(\n final_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> ComplianceRun:\n return ComplianceRun(\n id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n manifest_id=\"manifest-immut-1\",\n manifest_digest=\"digest-immut-1\",\n policy_snapshot_id=\"policy-immut-1\",\n registry_snapshot_id=\"registry-immut-1\",\n requested_by=\"tester\",\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n finished_at=datetime.now(UTC),\n status=RunStatus.SUCCEEDED,\n final_status=final_status,\n )\n\n\n# #endregion _terminal_run\n\n\n# #region test_report_builder_sets_immutable_snapshot_flag [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Ensure generated report payload is marked immutable and persisted as snapshot.\n# @PRE: Terminal run exists.\n# @POST: Built report has immutable=True and repository stores same immutable object.\ndef test_report_builder_sets_immutable_snapshot_flag():\n repository = CleanReleaseRepository()\n builder = ComplianceReportBuilder(repository)\n run = _terminal_run()\n\n report = builder.build_report_payload(run, [])\n persisted = builder.persist_report(report)\n\n assert report.immutable is True\n assert persisted.immutable is True\n assert repository.get_report(report.id) is persisted\n\n\n# #endregion test_report_builder_sets_immutable_snapshot_flag\n\n\n# #region test_repository_rejects_report_overwrite_for_same_report_id [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Define immutability contract that report snapshots cannot be overwritten by same identifier.\n# @PRE: Existing report with id is already persisted.\n# @POST: Second save for same report id is rejected with explicit immutability error.\ndef test_repository_rejects_report_overwrite_for_same_report_id():\n repository = CleanReleaseRepository()\n original = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n final_status=ComplianceDecision.PASSED,\n summary_json={\n \"operator_summary\": \"original\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n mutated = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-2\",\n candidate_id=\"cand-immut-2\",\n final_status=ComplianceDecision.ERROR,\n summary_json={\n \"operator_summary\": \"mutated\",\n \"violations_count\": 1,\n \"blocking_violations_count\": 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n\n repository.save_report(original)\n\n with pytest.raises(ValueError, match=\"immutable\"):\n repository.save_report(mutated)\n\n\n# #endregion test_repository_rejects_report_overwrite_for_same_report_id\n\n\n# #region test_audit_hooks_emit_append_only_event_stream [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.reason.call_count == 1\n assert mock_logger.reflect.call_count == 1\n assert mock_logger.explore.call_count == 1\n\n reason_msg = mock_logger.reason.call_args[0][0]\n reflect_msg = mock_logger.reflect.call_args[0][0]\n explore_msg = mock_logger.explore.call_args[0][0]\n\n assert reason_msg.startswith(\"clean-release preparation\")\n assert reflect_msg.startswith(\"clean-release check_run\")\n assert explore_msg.startswith(\"clean-release report_id\")\n\n\n# #endregion test_audit_hooks_emit_append_only_event_stream\n\n# #endregion TestReportAuditImmutability\n" + "body": "# #region TestReportAuditImmutability [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only\n# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.\n# @LAYER Tests\n# @INVARIANT: Built reports are immutable snapshots; audit hooks produce append-only event traces.\n\nfrom __future__ import annotations\n\nfrom datetime import UTC, datetime\nimport pytest\nfrom unittest.mock import patch\n\nfrom src.models.clean_release import (\n ComplianceReport,\n ComplianceRun,\n)\nfrom src.services.clean_release.audit_service import (\n audit_check_run,\n audit_preparation,\n audit_report,\n)\nfrom src.services.clean_release.enums import ComplianceDecision, RunStatus\nfrom src.services.clean_release.report_builder import ComplianceReportBuilder\nfrom src.services.clean_release.repository import CleanReleaseRepository\n\n\n# #region _terminal_run [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Build deterministic terminal run fixture for report snapshot tests.\n# @PRE: final_status is a valid ComplianceDecision value.\n# @POST: Returns a terminal ComplianceRun suitable for report generation.\ndef _terminal_run(\n final_status: ComplianceDecision = ComplianceDecision.PASSED,\n) -> ComplianceRun:\n return ComplianceRun(\n id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n manifest_id=\"manifest-immut-1\",\n manifest_digest=\"digest-immut-1\",\n policy_snapshot_id=\"policy-immut-1\",\n registry_snapshot_id=\"registry-immut-1\",\n requested_by=\"tester\",\n requested_at=datetime.now(UTC),\n started_at=datetime.now(UTC),\n finished_at=datetime.now(UTC),\n status=RunStatus.SUCCEEDED,\n final_status=final_status,\n )\n\n\n# #endregion _terminal_run\n\n\n# #region test_report_builder_sets_immutable_snapshot_flag [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Ensure generated report payload is marked immutable and persisted as snapshot.\n# @PRE: Terminal run exists.\n# @POST: Built report has immutable=True and repository stores same immutable object.\ndef test_report_builder_sets_immutable_snapshot_flag():\n repository = CleanReleaseRepository()\n builder = ComplianceReportBuilder(repository)\n run = _terminal_run()\n\n report = builder.build_report_payload(run, [])\n persisted = builder.persist_report(report)\n\n assert report.immutable is True\n assert persisted.immutable is True\n assert repository.get_report(report.id) is persisted\n\n\n# #endregion test_report_builder_sets_immutable_snapshot_flag\n\n\n# #region test_repository_rejects_report_overwrite_for_same_report_id [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Define immutability contract that report snapshots cannot be overwritten by same identifier.\n# @PRE: Existing report with id is already persisted.\n# @POST: Second save for same report id is rejected with explicit immutability error.\ndef test_repository_rejects_report_overwrite_for_same_report_id():\n repository = CleanReleaseRepository()\n original = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-1\",\n candidate_id=\"cand-immut-1\",\n final_status=ComplianceDecision.PASSED,\n summary_json={\n \"operator_summary\": \"original\",\n \"violations_count\": 0,\n \"blocking_violations_count\": 0,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n mutated = ComplianceReport(\n id=\"CCR-immut-fixed-id\",\n run_id=\"run-immut-2\",\n candidate_id=\"cand-immut-2\",\n final_status=ComplianceDecision.ERROR,\n summary_json={\n \"operator_summary\": \"mutated\",\n \"violations_count\": 1,\n \"blocking_violations_count\": 1,\n },\n generated_at=datetime.now(UTC),\n immutable=True,\n )\n\n repository.save_report(original)\n\n with pytest.raises(ValueError, match=\"immutable\"):\n repository.save_report(mutated)\n\n\n# #endregion test_repository_rejects_report_overwrite_for_same_report_id\n\n\n# #region test_audit_hooks_emit_append_only_event_stream [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestReportAuditImmutability\n# @PURPOSE: Verify audit hooks emit one event per action call and preserve call order.\n# @PRE: Logger backend is patched.\n# @POST: Three calls produce three ordered info entries with molecular prefixes.\n@patch(\"src.services.clean_release.audit_service.logger\")\ndef test_audit_hooks_emit_append_only_event_stream(mock_logger):\n audit_preparation(\"cand-immut-1\", \"PREPARED\")\n audit_check_run(\"run-immut-1\", \"PASSED\")\n audit_report(\"CCR-immut-1\", \"cand-immut-1\")\n\n assert mock_logger.reason.call_count == 1\n assert mock_logger.reflect.call_count == 1\n assert mock_logger.explore.call_count == 1\n\n reason_msg = mock_logger.reason.call_args[0][0]\n reflect_msg = mock_logger.reflect.call_args[0][0]\n explore_msg = mock_logger.explore.call_args[0][0]\n\n assert reason_msg.startswith(\"clean-release preparation\")\n assert reflect_msg.startswith(\"clean-release check_run\")\n assert explore_msg.startswith(\"clean-release report_id\")\n\n\n# #endregion test_audit_hooks_emit_append_only_event_stream\n\n# #endregion TestReportAuditImmutability\n" }, { "contract_id": "_terminal_run", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 29, - "end_line": 53, + "start_line": 28, + "end_line": 52, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51945,8 +52825,8 @@ "contract_id": "test_report_builder_sets_immutable_snapshot_flag", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 56, - "end_line": 74, + "start_line": 55, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51972,8 +52852,8 @@ "contract_id": "test_repository_rejects_report_overwrite_for_same_report_id", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 77, - "end_line": 117, + "start_line": 76, + "end_line": 116, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -51999,8 +52879,8 @@ "contract_id": "test_audit_hooks_emit_append_only_event_stream", "contract_type": "Function", "file_path": "backend/tests/services/clean_release/test_report_audit_immutability.py", - "start_line": 120, - "end_line": 144, + "start_line": 119, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52164,12 +53044,98 @@ "tier_source": "AutoCalculated", "body": "# #region test_sql_lab_launch_falls_back_to_legacy_execute_endpoint [C:2] [TYPE Function]\n# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.\n# @RELATION DEPENDS_ON ->[SupersetCompilationAdapter]\ndef test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():\n adapter, client = make_adapter()\n client.get_dataset.return_value = {\n \"result\": {\n \"id\": 55,\n \"schema\": \"public\",\n \"database\": {\"id\": 9},\n }\n }\n client.network.request.side_effect = [\n RuntimeError(\"sqllab execute unavailable\"),\n {\"result\": {\"id\": \"query-123\"}},\n ]\n payload = SqlLabLaunchPayload(\n session_id=\"sess-3\",\n dataset_id=55,\n preview_id=\"preview-9\",\n compiled_sql=\"SELECT * FROM sales\",\n template_params={\"limit\": 10},\n )\n\n sql_lab_ref = adapter.create_sql_lab_session(payload)\n\n assert sql_lab_ref == \"query-123\"\n assert client.network.request.call_count == 2\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n first_call = client.network.request.call_args_list[0].kwargs\n # @FRAGILE: Positional call assertion — ordering changes will break this test without indicating a real regression. Prefer content-based assertion.\n second_call = client.network.request.call_args_list[1].kwargs\n assert first_call[\"endpoint\"] == \"/sqllab/execute/\"\n assert second_call[\"endpoint\"] == \"/sql_lab/execute/\"\n\n\n# #endregion test_sql_lab_launch_falls_back_to_legacy_execute_endpoint\n" }, + { + "contract_id": "TestAlembicMigrations", + "contract_type": "Module", + "file_path": "backend/tests/test_alembic_migrations.py", + "start_line": 1, + "end_line": 126, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Verifies Alembic migration chain: fresh upgrade and legacy stamp.", + "COMPLEXITY": 3, + "INVARIANT": "Legacy database with existing tables can be stamped after adding boot-critical columns.", + "POST": "Schema matches expected state after upgrade.", + "PRE": "Alembic is installed, migration files exist in alembic/versions/.", + "REJECTED": "Full migration chain idempotency on SQLite — initial migration uses", + "TEST_INVARIANT": "alembic_stamp_legacy -> VERIFIED_BY: [test_legacy_database_stamp]" + }, + "relations": [ + { + "source_id": "TestAlembicMigrations", + "relation_type": "BINDS_TO", + "target_id": "ed310b33f02c", + "target_ref": "[ed310b33f02c]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region TestAlembicMigrations [C:3] [TYPE Module] [SEMANTICS tests, alembic, migration, schema, idempotent]\n# @BRIEF Verifies Alembic migration chain: fresh upgrade and legacy stamp.\n# @RELATION BINDS_TO -> [ed310b33f02c]\n# @INVARIANT Running `alembic upgrade head` twice is idempotent (no-op on second run) on PostgreSQL.\n# @INVARIANT Legacy database with existing tables can be stamped after adding boot-critical columns.\n# @PRE Alembic is installed, migration files exist in alembic/versions/.\n# @POST Schema matches expected state after upgrade.\n# @TEST_INVARIANT: alembic_fresh_upgrade -> VERIFIED_BY: [test_fresh_upgrade_creates_tables]\n# @TEST_INVARIANT: alembic_stamp_legacy -> VERIFIED_BY: [test_legacy_database_stamp]\n# @REJECTED Full migration chain idempotency on SQLite — initial migration uses\n# op.create_index() which conflicts with SQLite auto-created indexes\n# for UNIQUE constraints. Migrations are designed for PostgreSQL.\n# Legacy databases use stamp + selective raw SQL in entrypoint instead.\n\nimport os\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\n# Ensure backend/src is importable for model metadata\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n# Alembic migrations use PostgreSQL-specific features (CREATE INDEX may conflict\n# with SQLite auto-indexes for UNIQUE constraints). Skip full-chain tests on SQLite.\n_DB_BACKEND = os.environ.get(\"DATABASE_URL\", \"sqlite://\").split(\":\")[0]\n_REQUIRES_PG = pytest.mark.skipif(\n _DB_BACKEND != \"postgresql\",\n reason=\"Alembic migrations are PostgreSQL-specific; use postgresql:// DATABASE_URL to test\",\n)\n\n\n# #region test_fresh_upgrade_creates_tables [C:3] [TYPE Function]\n# @BRIEF Run alembic upgrade head on a fresh PostgreSQL DB and verify key tables exist.\n# @POST All expected tables exist after upgrade.\n# @TEST_INVARIANT: alembic_fresh_upgrade -> VERIFIED_BY: [test_fresh_upgrade_creates_tables]\n@_REQUIRES_PG\ndef test_fresh_upgrade_creates_tables() -> None:\n \"\"\"Fresh Alembic upgrade creates all expected tables (PostgreSQL only).\"\"\"\n from sqlalchemy import create_engine, inspect\n from sqlalchemy import text as sa_text\n backend_dir = Path(__file__).parent.parent\n _run_alembic_upgrade()\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n inspector = inspect(engine)\n tables = set(inspector.get_table_names())\n expected_tables = {\n \"environments\",\n \"translation_jobs\",\n \"users\",\n \"roles\",\n \"permissions\",\n \"task_records\",\n \"alembic_version\",\n }\n missing = expected_tables - tables\n assert not missing, f\"Missing tables after Alembic upgrade: {missing}\"\n with engine.connect() as conn:\n result = conn.execute(sa_text(\"SELECT version_num FROM alembic_version\"))\n version = result.scalar_one()\n assert version is not None and len(version) > 0\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n assert \"is_admin\" in roles_cols, \"roles.is_admin column missing after Alembic upgrade\"\n engine.dispose()\n# #endregion test_fresh_upgrade_creates_tables\n\n\n# #region test_legacy_database_stamp [C:2] [TYPE Function]\n# @BRIEF Pre-populate tables via create_all(), then verify entrypoint-style stamp works.\n# @POST alembic_version created, is_admin column present after stamp + raw SQL.\n@_REQUIRES_PG\ndef test_legacy_database_stamp() -> None:\n \"\"\"Alembic stamp works on a database that already has tables (PostgreSQL only).\"\"\"\n from sqlalchemy import create_engine, inspect\n from sqlalchemy import text as sa_text\n from src.models.mapping import Base\n from src.models import auth as _auth # noqa: F401\n from src.models import task as _task # noqa: F401\n from alembic.config import Config as AlembicConfig\n from alembic import command as alembic_command\n\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n # Drop alembic_version if exists to simulate legacy state\n with engine.begin() as conn:\n conn.execute(sa_text(\"DROP TABLE IF EXISTS alembic_version CASCADE\"))\n engine.dispose()\n # Tables already exist from previous runs (create_all)\n # Add is_admin column (simulating entrypoint raw SQL)\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n with engine.begin() as conn:\n inspector = inspect(conn)\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n if \"is_admin\" not in roles_cols:\n conn.execute(\n sa_text(\"ALTER TABLE roles ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE\")\n )\n conn.execute(sa_text(\"UPDATE roles SET is_admin = TRUE WHERE name = 'Admin'\"))\n engine.dispose()\n # Stamp head (entrypoint behavior for legacy DB)\n backend_dir = Path(__file__).parent.parent\n alembic_cfg = AlembicConfig(str(backend_dir / \"alembic.ini\"))\n alembic_cfg.set_main_option(\"script_location\", str(backend_dir / \"alembic\"))\n alembic_command.stamp(alembic_cfg, \"head\")\n # Verify\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n inspector = inspect(engine)\n tables = set(inspector.get_table_names())\n assert \"alembic_version\" in tables, \"alembic_version table not created by stamp\"\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n assert \"is_admin\" in roles_cols\n engine.dispose()\n# #endregion test_legacy_database_stamp\n\n\n# #region _run_alembic_upgrade [C:1] [TYPE Function]\ndef _run_alembic_upgrade() -> None:\n \"\"\"Run alembic upgrade head programmatically.\"\"\"\n from alembic.config import Config as AlembicConfig\n from alembic import command as alembic_command\n backend_dir = Path(__file__).parent.parent\n alembic_cfg = AlembicConfig(str(backend_dir / \"alembic.ini\"))\n alembic_cfg.set_main_option(\"script_location\", str(backend_dir / \"alembic\"))\n alembic_command.upgrade(alembic_cfg, \"head\")\n# #endregion _run_alembic_upgrade\n\n# #endregion TestAlembicMigrations\n" + }, + { + "contract_id": "test_fresh_upgrade_creates_tables", + "contract_type": "Function", + "file_path": "backend/tests/test_alembic_migrations.py", + "start_line": 33, + "end_line": 65, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Run alembic upgrade head on a fresh PostgreSQL DB and verify key tables exist.", + "COMPLEXITY": 3, + "POST": "All expected tables exist after upgrade.", + "TEST_INVARIANT": "alembic_fresh_upgrade -> VERIFIED_BY: [test_fresh_upgrade_creates_tables]" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region test_fresh_upgrade_creates_tables [C:3] [TYPE Function]\n# @BRIEF Run alembic upgrade head on a fresh PostgreSQL DB and verify key tables exist.\n# @POST All expected tables exist after upgrade.\n# @TEST_INVARIANT: alembic_fresh_upgrade -> VERIFIED_BY: [test_fresh_upgrade_creates_tables]\n@_REQUIRES_PG\ndef test_fresh_upgrade_creates_tables() -> None:\n \"\"\"Fresh Alembic upgrade creates all expected tables (PostgreSQL only).\"\"\"\n from sqlalchemy import create_engine, inspect\n from sqlalchemy import text as sa_text\n backend_dir = Path(__file__).parent.parent\n _run_alembic_upgrade()\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n inspector = inspect(engine)\n tables = set(inspector.get_table_names())\n expected_tables = {\n \"environments\",\n \"translation_jobs\",\n \"users\",\n \"roles\",\n \"permissions\",\n \"task_records\",\n \"alembic_version\",\n }\n missing = expected_tables - tables\n assert not missing, f\"Missing tables after Alembic upgrade: {missing}\"\n with engine.connect() as conn:\n result = conn.execute(sa_text(\"SELECT version_num FROM alembic_version\"))\n version = result.scalar_one()\n assert version is not None and len(version) > 0\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n assert \"is_admin\" in roles_cols, \"roles.is_admin column missing after Alembic upgrade\"\n engine.dispose()\n# #endregion test_fresh_upgrade_creates_tables\n" + }, + { + "contract_id": "test_legacy_database_stamp", + "contract_type": "Function", + "file_path": "backend/tests/test_alembic_migrations.py", + "start_line": 68, + "end_line": 112, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Pre-populate tables via create_all(), then verify entrypoint-style stamp works.", + "COMPLEXITY": 2, + "POST": "alembic_version created, is_admin column present after stamp + raw SQL." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region test_legacy_database_stamp [C:2] [TYPE Function]\n# @BRIEF Pre-populate tables via create_all(), then verify entrypoint-style stamp works.\n# @POST alembic_version created, is_admin column present after stamp + raw SQL.\n@_REQUIRES_PG\ndef test_legacy_database_stamp() -> None:\n \"\"\"Alembic stamp works on a database that already has tables (PostgreSQL only).\"\"\"\n from sqlalchemy import create_engine, inspect\n from sqlalchemy import text as sa_text\n from src.models.mapping import Base\n from src.models import auth as _auth # noqa: F401\n from src.models import task as _task # noqa: F401\n from alembic.config import Config as AlembicConfig\n from alembic import command as alembic_command\n\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n # Drop alembic_version if exists to simulate legacy state\n with engine.begin() as conn:\n conn.execute(sa_text(\"DROP TABLE IF EXISTS alembic_version CASCADE\"))\n engine.dispose()\n # Tables already exist from previous runs (create_all)\n # Add is_admin column (simulating entrypoint raw SQL)\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n with engine.begin() as conn:\n inspector = inspect(conn)\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n if \"is_admin\" not in roles_cols:\n conn.execute(\n sa_text(\"ALTER TABLE roles ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE\")\n )\n conn.execute(sa_text(\"UPDATE roles SET is_admin = TRUE WHERE name = 'Admin'\"))\n engine.dispose()\n # Stamp head (entrypoint behavior for legacy DB)\n backend_dir = Path(__file__).parent.parent\n alembic_cfg = AlembicConfig(str(backend_dir / \"alembic.ini\"))\n alembic_cfg.set_main_option(\"script_location\", str(backend_dir / \"alembic\"))\n alembic_command.stamp(alembic_cfg, \"head\")\n # Verify\n engine = create_engine(os.environ[\"DATABASE_URL\"])\n inspector = inspect(engine)\n tables = set(inspector.get_table_names())\n assert \"alembic_version\" in tables, \"alembic_version table not created by stamp\"\n roles_cols = {c[\"name\"] for c in inspector.get_columns(\"roles\")}\n assert \"is_admin\" in roles_cols\n engine.dispose()\n# #endregion test_legacy_database_stamp\n" + }, + { + "contract_id": "_run_alembic_upgrade", + "contract_type": "Function", + "file_path": "backend/tests/test_alembic_migrations.py", + "start_line": 115, + "end_line": 124, + "tier": "TIER_1", + "complexity": 1, + "metadata": { + "COMPLEXITY": 1 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "# #region _run_alembic_upgrade [C:1] [TYPE Function]\ndef _run_alembic_upgrade() -> None:\n \"\"\"Run alembic upgrade head programmatically.\"\"\"\n from alembic.config import Config as AlembicConfig\n from alembic import command as alembic_command\n backend_dir = Path(__file__).parent.parent\n alembic_cfg = AlembicConfig(str(backend_dir / \"alembic.ini\"))\n alembic_cfg.set_main_option(\"script_location\", str(backend_dir / \"alembic\"))\n alembic_command.upgrade(alembic_cfg, \"head\")\n# #endregion _run_alembic_upgrade\n" + }, { "contract_id": "TestAPIKeyAuth", "contract_type": "Module", "file_path": "backend/tests/test_api_key_auth.py", "start_line": 1, - "end_line": 255, + "end_line": 261, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -52180,14 +53146,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestAPIKeyAuth [C:3] [TYPE Module] [SEMANTICS test, api_key, auth, dependency]\n# @BRIEF Contract tests for API key authentication — valid key, invalid, revoked, expired, missing permission,\n# environment scoping, JWT precedence. Uses TestClient with dependency overrides.\n# @RELATION BINDS_TO -> [APIKeyUtilities]\n# @RELATION BINDS_TO -> [APIKeyModel]\n# @RELATION BINDS_TO -> [MaintenanceRoutesModule]\n# @TEST_CONTRACT: get_api_key_principal returns APIKeyPrincipal for valid key, None for no header, 401 for invalid/revoked/expired.\n# @TEST_CONTRACT: require_api_key_or_jwt rejects invalid/revoked/expired keys, checks permissions, enforces environment scope.\n# @TEST_EDGE: missing_header -> returns None\n# @TEST_EDGE: invalid_key -> 401\n# @TEST_EDGE: revoked_key -> 401\n# @TEST_EDGE: expired_key -> 401\n# @TEST_EDGE: missing_permission -> 403\n# @TEST_EDGE: environment_scope_mismatch -> 400\n# @TEST_EDGE: jwt_precedence -> JWT takes precedence over API key\nfrom datetime import datetime, timezone, timedelta\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom fastapi import HTTPException, status\nfrom fastapi.testclient import TestClient\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create an in-memory SQLite session with the api_keys table.\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n@pytest.fixture\ndef valid_api_key(db_session):\n \"\"\"Create a valid API key and return its raw value, prefix, and database row.\"\"\"\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n db_session.add(api_key)\n db_session.commit()\n return raw, api_key\n\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient.\"\"\"\n from src.app import app\n\n return TestClient(app)\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency with in-memory SQLite session (same pattern as test_maintenance_api).\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n from src.dependencies import get_db\n from src.app import app\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Add default maintenance settings (needed by /api/maintenance endpoints)\n from src.models.maintenance import MaintenanceSettings, DashboardScope\n\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=\"test-env\",\n display_timezone=\"UTC\",\n banner_template=\"Test: {message} ({start_time}-{end_time})\",\n dashboard_scope=DashboardScope.PUBLISHED_ONLY,\n excluded_dashboard_ids=[],\n forced_dashboard_ids=[],\n )\n session.add(settings)\n session.commit()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n@pytest.fixture\ndef mock_task_manager():\n \"\"\"Patch get_task_manager to return a mock.\"\"\"\n from src.dependencies import get_task_manager\n from src.app import app\n\n mock_tm = MagicMock()\n mock_task = MagicMock()\n mock_task.id = \"test-task-id-123\"\n mock_tm.create_task = AsyncMock(return_value=mock_task)\n\n app.dependency_overrides[get_task_manager] = lambda: mock_tm\n yield mock_tm\n app.dependency_overrides.pop(get_task_manager, None)\n\n\n# ── Tests for get_api_key_principal ───────────────────────────\n\nclass TestGetApiKeyPrincipal:\n \"\"\"Contract tests for get_api_key_principal dependency.\"\"\"\n\n # #region test_no_header_returns_none [C:2] [TYPE Function]\n # @BRIEF No X-API-Key header → returns None.\n @pytest.mark.asyncio\n async def test_no_header_returns_none(self):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n # Mock a request without the header\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = None\n mock_db = MagicMock()\n\n result = await get_api_key_principal(mock_request, mock_db)\n assert result is None\n\n # #endregion test_no_header_returns_none\n\n # #region test_valid_key_returns_principal [C:2] [TYPE Function]\n # @BRIEF Valid API key → returns APIKeyPrincipal with correct fields.\n @pytest.mark.asyncio\n async def test_valid_key_returns_principal(self, db_session, valid_api_key):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n raw_key, api_key_row = valid_api_key\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw_key\n\n result = await get_api_key_principal(mock_request, db_session)\n assert result is not None\n assert result.name == \"Test Key\"\n assert result.api_key_id == api_key_row.id\n assert result.environment_id is None\n assert \"maintenance:start\" in result.permissions\n\n # #endregion test_valid_key_returns_principal\n\n # #region test_invalid_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Invalid API key → 401.\n @pytest.mark.asyncio\n async def test_invalid_key_raises_401(self, db_session):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = \"ssk_invalid_key_that_does_not_exist\"\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_invalid_key_raises_401\n\n # #region test_revoked_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Revoked (active=False) API key → 401.\n @pytest.mark.asyncio\n async def test_revoked_key_raises_401(self, db_session):\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n from fastapi import Request\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Revoked Key\",\n permissions=[\"maintenance:start\"],\n active=False, # revoked\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_revoked_key_raises_401\n\n # #region test_expired_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n @pytest.mark.asyncio\n async def test_expired_key_raises_401(self, db_session):\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n from fastapi import Request\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(timezone.utc) - timedelta(hours=1), # expired\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_expired_key_raises_401\n\n\n# #endregion TestAPIKeyAuth\n" + "body": "# #region TestAPIKeyAuth [C:3] [TYPE Module] [SEMANTICS test, api_key, auth, dependency]\n# @BRIEF Contract tests for API key authentication — valid key, invalid, revoked, expired, missing permission,\n# environment scoping, JWT precedence. Uses TestClient with dependency overrides.\n# @RELATION BINDS_TO -> [APIKeyUtilities]\n# @RELATION BINDS_TO -> [APIKeyModel]\n# @RELATION BINDS_TO -> [MaintenanceRoutesModule]\n# @TEST_CONTRACT: get_api_key_principal returns APIKeyPrincipal for valid key, None for no header, 401 for invalid/revoked/expired.\n# @TEST_CONTRACT: require_api_key_or_jwt rejects invalid/revoked/expired keys, checks permissions, enforces environment scope.\n# @TEST_EDGE: missing_header -> returns None\n# @TEST_EDGE: invalid_key -> 401\n# @TEST_EDGE: revoked_key -> 401\n# @TEST_EDGE: expired_key -> 401\n# @TEST_EDGE: missing_permission -> 403\n# @TEST_EDGE: environment_scope_mismatch -> 400\n# @TEST_EDGE: jwt_precedence -> JWT takes precedence over API key\nfrom datetime import UTC, datetime, timedelta\nimport pytest\nfrom unittest.mock import AsyncMock, MagicMock\n\nfrom fastapi import HTTPException\nfrom fastapi.testclient import TestClient\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create an in-memory SQLite session with the api_keys table.\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n\n\n@pytest.fixture\ndef valid_api_key(db_session):\n \"\"\"Create a valid API key and return its raw value, prefix, and database row.\"\"\"\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n db_session.add(api_key)\n db_session.commit()\n return raw, api_key\n\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient.\"\"\"\n from src.app import app\n\n return TestClient(app)\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency with in-memory SQLite session (same pattern as test_maintenance_api).\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.app import app\n from src.dependencies import get_db\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Add default maintenance settings (needed by /api/maintenance endpoints)\n from src.models.maintenance import DashboardScope, MaintenanceSettings\n\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=\"test-env\",\n display_timezone=\"UTC\",\n banner_template=\"Test: {message} ({start_time}-{end_time})\",\n dashboard_scope=DashboardScope.PUBLISHED_ONLY,\n excluded_dashboard_ids=[],\n forced_dashboard_ids=[],\n )\n session.add(settings)\n session.commit()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n@pytest.fixture\ndef mock_task_manager():\n \"\"\"Patch get_task_manager to return a mock.\"\"\"\n from src.app import app\n from src.dependencies import get_task_manager\n\n mock_tm = MagicMock()\n mock_task = MagicMock()\n mock_task.id = \"test-task-id-123\"\n mock_tm.create_task = AsyncMock(return_value=mock_task)\n\n app.dependency_overrides[get_task_manager] = lambda: mock_tm\n yield mock_tm\n app.dependency_overrides.pop(get_task_manager, None)\n\n\n# ── Tests for get_api_key_principal ───────────────────────────\n\nclass TestGetApiKeyPrincipal:\n \"\"\"Contract tests for get_api_key_principal dependency.\"\"\"\n\n # #region test_no_header_returns_none [C:2] [TYPE Function]\n # @BRIEF No X-API-Key header → returns None.\n @pytest.mark.asyncio\n async def test_no_header_returns_none(self):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n # Mock a request without the header\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = None\n mock_db = MagicMock()\n\n result = await get_api_key_principal(mock_request, mock_db)\n assert result is None\n\n # #endregion test_no_header_returns_none\n\n # #region test_valid_key_returns_principal [C:2] [TYPE Function]\n # @BRIEF Valid API key → returns APIKeyPrincipal with correct fields.\n @pytest.mark.asyncio\n async def test_valid_key_returns_principal(self, db_session, valid_api_key):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n raw_key, api_key_row = valid_api_key\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw_key\n\n result = await get_api_key_principal(mock_request, db_session)\n assert result is not None\n assert result.name == \"Test Key\"\n assert result.api_key_id == api_key_row.id\n assert result.environment_id is None\n assert \"maintenance:start\" in result.permissions\n\n # #endregion test_valid_key_returns_principal\n\n # #region test_invalid_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Invalid API key → 401.\n @pytest.mark.asyncio\n async def test_invalid_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = \"ssk_invalid_key_that_does_not_exist\"\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_invalid_key_raises_401\n\n # #region test_revoked_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Revoked (active=False) API key → 401.\n @pytest.mark.asyncio\n async def test_revoked_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Revoked Key\",\n permissions=[\"maintenance:start\"],\n active=False, # revoked\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_revoked_key_raises_401\n\n # #region test_expired_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n @pytest.mark.asyncio\n async def test_expired_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(UTC) - timedelta(hours=1), # expired\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_expired_key_raises_401\n\n\n# #endregion TestAPIKeyAuth\n" }, { "contract_id": "test_no_header_returns_none", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 142, - "end_line": 157, + "start_line": 143, + "end_line": 159, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52198,14 +53164,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_no_header_returns_none [C:2] [TYPE Function]\n # @BRIEF No X-API-Key header → returns None.\n @pytest.mark.asyncio\n async def test_no_header_returns_none(self):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n # Mock a request without the header\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = None\n mock_db = MagicMock()\n\n result = await get_api_key_principal(mock_request, mock_db)\n assert result is None\n\n # #endregion test_no_header_returns_none\n" + "body": " # #region test_no_header_returns_none [C:2] [TYPE Function]\n # @BRIEF No X-API-Key header → returns None.\n @pytest.mark.asyncio\n async def test_no_header_returns_none(self):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n # Mock a request without the header\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = None\n mock_db = MagicMock()\n\n result = await get_api_key_principal(mock_request, mock_db)\n assert result is None\n\n # #endregion test_no_header_returns_none\n" }, { "contract_id": "test_valid_key_returns_principal", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 159, - "end_line": 177, + "start_line": 161, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52216,14 +53182,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_valid_key_returns_principal [C:2] [TYPE Function]\n # @BRIEF Valid API key → returns APIKeyPrincipal with correct fields.\n @pytest.mark.asyncio\n async def test_valid_key_returns_principal(self, db_session, valid_api_key):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n raw_key, api_key_row = valid_api_key\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw_key\n\n result = await get_api_key_principal(mock_request, db_session)\n assert result is not None\n assert result.name == \"Test Key\"\n assert result.api_key_id == api_key_row.id\n assert result.environment_id is None\n assert \"maintenance:start\" in result.permissions\n\n # #endregion test_valid_key_returns_principal\n" + "body": " # #region test_valid_key_returns_principal [C:2] [TYPE Function]\n # @BRIEF Valid API key → returns APIKeyPrincipal with correct fields.\n @pytest.mark.asyncio\n async def test_valid_key_returns_principal(self, db_session, valid_api_key):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n raw_key, api_key_row = valid_api_key\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw_key\n\n result = await get_api_key_principal(mock_request, db_session)\n assert result is not None\n assert result.name == \"Test Key\"\n assert result.api_key_id == api_key_row.id\n assert result.environment_id is None\n assert \"maintenance:start\" in result.permissions\n\n # #endregion test_valid_key_returns_principal\n" }, { "contract_id": "test_invalid_key_raises_401", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 179, - "end_line": 193, + "start_line": 182, + "end_line": 197, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52234,14 +53200,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_invalid_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Invalid API key → 401.\n @pytest.mark.asyncio\n async def test_invalid_key_raises_401(self, db_session):\n from src.dependencies import get_api_key_principal\n from fastapi import Request\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = \"ssk_invalid_key_that_does_not_exist\"\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_invalid_key_raises_401\n" + "body": " # #region test_invalid_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Invalid API key → 401.\n @pytest.mark.asyncio\n async def test_invalid_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.dependencies import get_api_key_principal\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = \"ssk_invalid_key_that_does_not_exist\"\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_invalid_key_raises_401\n" }, { "contract_id": "test_revoked_key_raises_401", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 195, - "end_line": 222, + "start_line": 199, + "end_line": 227, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52252,14 +53218,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_revoked_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Revoked (active=False) API key → 401.\n @pytest.mark.asyncio\n async def test_revoked_key_raises_401(self, db_session):\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n from fastapi import Request\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Revoked Key\",\n permissions=[\"maintenance:start\"],\n active=False, # revoked\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_revoked_key_raises_401\n" + "body": " # #region test_revoked_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Revoked (active=False) API key → 401.\n @pytest.mark.asyncio\n async def test_revoked_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Revoked Key\",\n permissions=[\"maintenance:start\"],\n active=False, # revoked\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_revoked_key_raises_401\n" }, { "contract_id": "test_expired_key_raises_401", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 224, - "end_line": 252, + "start_line": 229, + "end_line": 258, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52270,14 +53236,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_expired_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n @pytest.mark.asyncio\n async def test_expired_key_raises_401(self, db_session):\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n from fastapi import Request\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(timezone.utc) - timedelta(hours=1), # expired\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_expired_key_raises_401\n" + "body": " # #region test_expired_key_raises_401 [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n @pytest.mark.asyncio\n async def test_expired_key_raises_401(self, db_session):\n from fastapi import Request\n\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_api_key_principal\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(UTC) - timedelta(hours=1), # expired\n )\n db_session.add(api_key)\n db_session.commit()\n\n mock_request = MagicMock(spec=Request)\n mock_request.headers.get.return_value = raw\n\n with pytest.raises(HTTPException) as exc:\n await get_api_key_principal(mock_request, db_session)\n assert exc.value.status_code == 401\n\n # #endregion test_expired_key_raises_401\n" }, { "contract_id": "test_api_key_auth_valid", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 291, - "end_line": 306, + "start_line": 297, + "end_line": 312, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52294,8 +53260,8 @@ "contract_id": "test_api_key_auth_wrong_permission", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 308, - "end_line": 323, + "start_line": 314, + "end_line": 329, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52312,8 +53278,8 @@ "contract_id": "test_api_key_auth_revoked", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 325, - "end_line": 350, + "start_line": 331, + "end_line": 356, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52330,8 +53296,8 @@ "contract_id": "test_api_key_auth_expired", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 352, - "end_line": 378, + "start_line": 358, + "end_line": 384, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52342,14 +53308,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_api_key_auth_expired [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n def test_api_key_auth_expired(self, client, mock_db, mock_task_manager):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(timezone.utc) - timedelta(hours=1),\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.post(\n self.API_START_URL,\n json=self.START_PAYLOAD,\n headers={\"X-API-Key\": raw},\n )\n assert response.status_code == 401\n assert \"expired\" in response.json().get(\"detail\", \"\").lower()\n\n # #endregion test_api_key_auth_expired\n" + "body": " # #region test_api_key_auth_expired [C:2] [TYPE Function]\n # @BRIEF Expired API key → 401.\n def test_api_key_auth_expired(self, client, mock_db, mock_task_manager):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Expired Key\",\n permissions=[\"maintenance:start\"],\n active=True,\n expires_at=datetime.now(UTC) - timedelta(hours=1),\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.post(\n self.API_START_URL,\n json=self.START_PAYLOAD,\n headers={\"X-API-Key\": raw},\n )\n assert response.status_code == 401\n assert \"expired\" in response.json().get(\"detail\", \"\").lower()\n\n # #endregion test_api_key_auth_expired\n" }, { "contract_id": "test_api_key_auth_invalid", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 380, - "end_line": 391, + "start_line": 386, + "end_line": 397, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52366,8 +53332,8 @@ "contract_id": "test_api_key_auth_environment_scope", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 393, - "end_line": 415, + "start_line": 399, + "end_line": 421, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52384,8 +53350,8 @@ "contract_id": "test_api_key_auth_environment_scope_ok", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 417, - "end_line": 438, + "start_line": 423, + "end_line": 444, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52402,8 +53368,8 @@ "contract_id": "test_jwt_precedence", "contract_type": "Function", "file_path": "backend/tests/test_api_key_auth.py", - "start_line": 440, - "end_line": 497, + "start_line": 446, + "end_line": 503, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52414,14 +53380,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_jwt_precedence [C:2] [TYPE Function]\n # @BRIEF Both valid API key + valid JWT → JWT takes precedence.\n def test_jwt_precedence(self, client, mock_db, mock_task_manager):\n from src.core.auth.jwt import create_access_token\n from src.models.auth import User, Role\n from src.dependencies import get_auth_db\n from src.app import app\n\n raw_key = self._create_api_key(\n mock_db,\n environment_id=\"ss-dev\",\n permissions=[\"maintenance:start\"],\n )\n\n # Create a JWT user with Admin role (full access)\n role = Role(name=\"Admin\", description=\"Admin role\")\n mock_db.add(role)\n mock_db.commit()\n\n user = User(\n username=\"jwt-user\",\n password_hash=\"nohash\",\n auth_source=\"LOCAL\",\n is_active=True,\n )\n user.roles.append(role)\n mock_db.add(user)\n mock_db.commit()\n\n # Create a valid JWT token\n token = create_access_token(data={\"sub\": \"jwt-user\"})\n\n # Override get_auth_db to use our mock_db for auth queries too\n # The auth DB and main DB are separate in production, but for tests\n # we use the same in-memory DB\n\n def _get_auth_db_override():\n yield mock_db\n\n app.dependency_overrides[get_auth_db] = _get_auth_db_override\n\n # Send request with BOTH X-API-Key and Authorization: Bearer\n response = client.post(\n self.API_START_URL,\n json=self.START_PAYLOAD,\n headers={\n \"X-API-Key\": raw_key,\n \"Authorization\": f\"Bearer {token}\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n\n # Cleanup\n app.dependency_overrides.pop(get_auth_db, None)\n\n # #endregion test_jwt_precedence\n" + "body": " # #region test_jwt_precedence [C:2] [TYPE Function]\n # @BRIEF Both valid API key + valid JWT → JWT takes precedence.\n def test_jwt_precedence(self, client, mock_db, mock_task_manager):\n from src.app import app\n from src.core.auth.jwt import create_access_token\n from src.dependencies import get_auth_db\n from src.models.auth import Role, User\n\n raw_key = self._create_api_key(\n mock_db,\n environment_id=\"ss-dev\",\n permissions=[\"maintenance:start\"],\n )\n\n # Create a JWT user with Admin role (full access)\n role = Role(name=\"Admin\", description=\"Admin role\")\n mock_db.add(role)\n mock_db.commit()\n\n user = User(\n username=\"jwt-user\",\n password_hash=\"nohash\",\n auth_source=\"LOCAL\",\n is_active=True,\n )\n user.roles.append(role)\n mock_db.add(user)\n mock_db.commit()\n\n # Create a valid JWT token\n token = create_access_token(data={\"sub\": \"jwt-user\"})\n\n # Override get_auth_db to use our mock_db for auth queries too\n # The auth DB and main DB are separate in production, but for tests\n # we use the same in-memory DB\n\n def _get_auth_db_override():\n yield mock_db\n\n app.dependency_overrides[get_auth_db] = _get_auth_db_override\n\n # Send request with BOTH X-API-Key and Authorization: Bearer\n response = client.post(\n self.API_START_URL,\n json=self.START_PAYLOAD,\n headers={\n \"X-API-Key\": raw_key,\n \"Authorization\": f\"Bearer {token}\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n\n # Cleanup\n app.dependency_overrides.pop(get_auth_db, None)\n\n # #endregion test_jwt_precedence\n" }, { "contract_id": "TestAPIKeyModel", "contract_type": "Module", "file_path": "backend/tests/test_api_key_model.py", "start_line": 1, - "end_line": 122, + "end_line": 123, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52441,13 +53407,13 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestAPIKeyModel [C:2] [TYPE Module] [SEMANTICS test, api_key, model]\n# @BRIEF Contract tests for the APIKey SQLAlchemy model — creation, hash storage, and field constraints.\n# @RELATION BINDS_TO -> [APIKeyModel]\n# @TEST_CONTRACT: APIKey model stores SHA-256 hash and prefix; raw key never persisted.\n# @TEST_EDGE: key_hash is unique and indexed\n# @TEST_EDGE: prefix is exactly 11 chars (\"ssk_\" + 7)\n# @TEST_EDGE: active defaults to True\nimport pytest\nfrom datetime import datetime, timezone\n\n\n# #region clean_db [C:1] [TYPE Fixture]\n# @BRIEF In-memory SQLite fixture for APIKey model tests.\n@pytest.fixture\ndef clean_db():\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #endregion clean_db\n\n\nclass TestAPIKeyModel:\n \"\"\"Verify APIKey model fields and constraints.\"\"\"\n\n # #region test_create_api_key [C:2] [TYPE Function]\n # @BRIEF Create APIKey with required fields and verify defaults.\n def test_create_api_key(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"a\" * 64,\n prefix=\"ssk_abc1234\",\n name=\"Test Key\",\n permissions=[\"maintenance:start\"],\n )\n clean_db.add(api_key)\n clean_db.commit()\n clean_db.refresh(api_key)\n\n assert api_key.id is not None\n assert api_key.key_hash == \"a\" * 64\n assert api_key.prefix == \"ssk_abc1234\"\n assert api_key.name == \"Test Key\"\n assert api_key.permissions == [\"maintenance:start\"]\n assert api_key.active is True\n assert api_key.created_at is not None\n assert api_key.environment_id is None\n assert api_key.expires_at is None\n assert api_key.last_used_at is None\n # #endregion test_create_api_key\n\n # #region test_key_hash_unique [C:2] [TYPE Function]\n # @BRIEF key_hash is unique — duplicate raises IntegrityError.\n def test_key_hash_unique(self, clean_db):\n from sqlalchemy.exc import IntegrityError\n from src.models.api_key import APIKey\n\n clean_db.add(APIKey(\n key_hash=\"b\" * 64,\n prefix=\"ssk_xyz7890\",\n name=\"Key 1\",\n permissions=[\"maintenance:start\"],\n ))\n clean_db.commit()\n\n with pytest.raises(IntegrityError):\n clean_db.add(APIKey(\n key_hash=\"b\" * 64, # Same hash\n prefix=\"ssk_def5678\",\n name=\"Key 2\",\n permissions=[\"maintenance:end\"],\n ))\n clean_db.commit()\n # #endregion test_key_hash_unique\n\n # #region test_prefix_length [C:2] [TYPE Function]\n # @BRIEF prefix is exactly 11 characters.\n def test_prefix_length(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"c\" * 64,\n prefix=\"ssk_short1\", # 10 chars — should be ok in SQL\n name=\"Short Prefix\",\n permissions=[\"maintenance:start\"],\n )\n clean_db.add(api_key)\n clean_db.commit()\n # The DB allows any prefix length; the app layer enforces 11 chars\n # via generate_api_key. Model just stores what it's given.\n assert len(api_key.prefix) <= 11\n # #endregion test_prefix_length\n\n # #region test_active_default_true [C:2] [TYPE Function]\n # @BRIEF active column defaults to True.\n def test_active_default_true(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"d\" * 64,\n prefix=\"ssk_def1234\",\n name=\"Default Active\",\n permissions=[],\n )\n clean_db.add(api_key)\n clean_db.commit()\n\n assert api_key.active is True\n # #endregion test_active_default_true\n# #endregion TestAPIKeyModel\n" + "body": "# #region TestAPIKeyModel [C:2] [TYPE Module] [SEMANTICS test, api_key, model]\n# @BRIEF Contract tests for the APIKey SQLAlchemy model — creation, hash storage, and field constraints.\n# @RELATION BINDS_TO -> [APIKeyModel]\n# @TEST_CONTRACT: APIKey model stores SHA-256 hash and prefix; raw key never persisted.\n# @TEST_EDGE: key_hash is unique and indexed\n# @TEST_EDGE: prefix is exactly 11 chars (\"ssk_\" + 7)\n# @TEST_EDGE: active defaults to True\nimport pytest\n\n\n# #region clean_db [C:1] [TYPE Fixture]\n# @BRIEF In-memory SQLite fixture for APIKey model tests.\n@pytest.fixture\ndef clean_db():\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #endregion clean_db\n\n\nclass TestAPIKeyModel:\n \"\"\"Verify APIKey model fields and constraints.\"\"\"\n\n # #region test_create_api_key [C:2] [TYPE Function]\n # @BRIEF Create APIKey with required fields and verify defaults.\n def test_create_api_key(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"a\" * 64,\n prefix=\"ssk_abc1234\",\n name=\"Test Key\",\n permissions=[\"maintenance:start\"],\n )\n clean_db.add(api_key)\n clean_db.commit()\n clean_db.refresh(api_key)\n\n assert api_key.id is not None\n assert api_key.key_hash == \"a\" * 64\n assert api_key.prefix == \"ssk_abc1234\"\n assert api_key.name == \"Test Key\"\n assert api_key.permissions == [\"maintenance:start\"]\n assert api_key.active is True\n assert api_key.created_at is not None\n assert api_key.environment_id is None\n assert api_key.expires_at is None\n assert api_key.last_used_at is None\n # #endregion test_create_api_key\n\n # #region test_key_hash_unique [C:2] [TYPE Function]\n # @BRIEF key_hash is unique — duplicate raises IntegrityError.\n def test_key_hash_unique(self, clean_db):\n from sqlalchemy.exc import IntegrityError\n\n from src.models.api_key import APIKey\n\n clean_db.add(APIKey(\n key_hash=\"b\" * 64,\n prefix=\"ssk_xyz7890\",\n name=\"Key 1\",\n permissions=[\"maintenance:start\"],\n ))\n clean_db.commit()\n\n with pytest.raises(IntegrityError):\n clean_db.add(APIKey(\n key_hash=\"b\" * 64, # Same hash\n prefix=\"ssk_def5678\",\n name=\"Key 2\",\n permissions=[\"maintenance:end\"],\n ))\n clean_db.commit()\n # #endregion test_key_hash_unique\n\n # #region test_prefix_length [C:2] [TYPE Function]\n # @BRIEF prefix is exactly 11 characters.\n def test_prefix_length(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"c\" * 64,\n prefix=\"ssk_short1\", # 10 chars — should be ok in SQL\n name=\"Short Prefix\",\n permissions=[\"maintenance:start\"],\n )\n clean_db.add(api_key)\n clean_db.commit()\n # The DB allows any prefix length; the app layer enforces 11 chars\n # via generate_api_key. Model just stores what it's given.\n assert len(api_key.prefix) <= 11\n # #endregion test_prefix_length\n\n # #region test_active_default_true [C:2] [TYPE Function]\n # @BRIEF active column defaults to True.\n def test_active_default_true(self, clean_db):\n from src.models.api_key import APIKey\n\n api_key = APIKey(\n key_hash=\"d\" * 64,\n prefix=\"ssk_def1234\",\n name=\"Default Active\",\n permissions=[],\n )\n clean_db.add(api_key)\n clean_db.commit()\n\n assert api_key.active is True\n # #endregion test_active_default_true\n# #endregion TestAPIKeyModel\n" }, { "contract_id": "clean_db", "contract_type": "Fixture", "file_path": "backend/tests/test_api_key_model.py", - "start_line": 12, + "start_line": 11, "end_line": 31, "tier": "TIER_1", "complexity": 1, @@ -52459,7 +53425,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region clean_db [C:1] [TYPE Fixture]\n# @BRIEF In-memory SQLite fixture for APIKey model tests.\n@pytest.fixture\ndef clean_db():\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #endregion clean_db\n" + "body": "# #region clean_db [C:1] [TYPE Fixture]\n# @BRIEF In-memory SQLite fixture for APIKey model tests.\n@pytest.fixture\ndef clean_db():\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n yield session\n session.close()\n# #endregion clean_db\n" }, { "contract_id": "test_create_api_key", @@ -52484,7 +53450,7 @@ "contract_type": "Function", "file_path": "backend/tests/test_api_key_model.py", "start_line": 64, - "end_line": 86, + "end_line": 87, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52495,14 +53461,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_key_hash_unique [C:2] [TYPE Function]\n # @BRIEF key_hash is unique — duplicate raises IntegrityError.\n def test_key_hash_unique(self, clean_db):\n from sqlalchemy.exc import IntegrityError\n from src.models.api_key import APIKey\n\n clean_db.add(APIKey(\n key_hash=\"b\" * 64,\n prefix=\"ssk_xyz7890\",\n name=\"Key 1\",\n permissions=[\"maintenance:start\"],\n ))\n clean_db.commit()\n\n with pytest.raises(IntegrityError):\n clean_db.add(APIKey(\n key_hash=\"b\" * 64, # Same hash\n prefix=\"ssk_def5678\",\n name=\"Key 2\",\n permissions=[\"maintenance:end\"],\n ))\n clean_db.commit()\n # #endregion test_key_hash_unique\n" + "body": " # #region test_key_hash_unique [C:2] [TYPE Function]\n # @BRIEF key_hash is unique — duplicate raises IntegrityError.\n def test_key_hash_unique(self, clean_db):\n from sqlalchemy.exc import IntegrityError\n\n from src.models.api_key import APIKey\n\n clean_db.add(APIKey(\n key_hash=\"b\" * 64,\n prefix=\"ssk_xyz7890\",\n name=\"Key 1\",\n permissions=[\"maintenance:start\"],\n ))\n clean_db.commit()\n\n with pytest.raises(IntegrityError):\n clean_db.add(APIKey(\n key_hash=\"b\" * 64, # Same hash\n prefix=\"ssk_def5678\",\n name=\"Key 2\",\n permissions=[\"maintenance:end\"],\n ))\n clean_db.commit()\n # #endregion test_key_hash_unique\n" }, { "contract_id": "test_prefix_length", "contract_type": "Function", "file_path": "backend/tests/test_api_key_model.py", - "start_line": 88, - "end_line": 104, + "start_line": 89, + "end_line": 105, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52519,8 +53485,8 @@ "contract_id": "test_active_default_true", "contract_type": "Function", "file_path": "backend/tests/test_api_key_model.py", - "start_line": 106, - "end_line": 121, + "start_line": 107, + "end_line": 122, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52538,7 +53504,7 @@ "contract_type": "Module", "file_path": "backend/tests/test_api_key_routes.py", "start_line": 1, - "end_line": 258, + "end_line": 256, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -52558,14 +53524,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestAPIKeyRoutes [C:3] [TYPE Module] [SEMANTICS test, api_key, routes, crud]\n# @BRIEF Contract tests for admin API key CRUD endpoints — list, create (raw key present), revoke, idempotent revoke.\n# @RELATION BINDS_TO -> [AdminApiKeyRoutes]\n# @TEST_CONTRACT: GET /api/admin/api-keys -> list of keys without raw_key or key_hash\n# @TEST_CONTRACT: POST /api/admin/api-keys -> 201 with raw_key in response\n# @TEST_CONTRACT: DELETE /api/admin/api-keys/{id} -> 200 with status=revoked\n# @TEST_EDGE: revoked_key_delete -> 404\n# @TEST_EDGE: missing_name -> 422\n# @TEST_EDGE: missing_permissions -> 422\nimport json\nimport sys\nfrom datetime import datetime, timezone, timedelta\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom fastapi import status\nfrom fastapi.testclient import TestClient\n\n# Patch GitService at module level to prevent /app/storage/repositories error\n_mock_git_svc_cls = MagicMock()\n_mock_git_svc_cls.return_value = MagicMock()\nsys.modules['src.services.git._base'] = MagicMock(GitService=_mock_git_svc_cls)\nsys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient with all dependencies mocked.\"\"\"\n from src.app import app\n return TestClient(app)\n\n\ndef _mock_admin_auth():\n \"\"\"Apply dependency overrides for admin authentication.\"\"\"\n from src.dependencies import get_current_user\n from src.app import app\n\n mock_role = MagicMock()\n mock_role.name = \"Admin\"\n mock_role.permissions = []\n\n mock_user = MagicMock()\n mock_user.username = \"test-admin\"\n mock_user.roles = [mock_role]\n\n async def _mock_get_current_user():\n return mock_user\n\n app.dependency_overrides[get_current_user] = _mock_get_current_user\n\n\ndef _clear_overrides():\n from src.app import app\n app.dependency_overrides = {}\n\n\n@pytest.fixture(autouse=True)\ndef auth_mock():\n _mock_admin_auth()\n yield\n _clear_overrides()\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency with in-memory SQLite session.\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n from src.dependencies import get_db\n from src.app import app\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n# ── Tests ─────────────────────────────────────────────────────\n\nclass TestListApiKeys:\n \"\"\"Contract tests for GET /api/admin/api-keys.\"\"\"\n\n # #region test_list_empty [C:2] [TYPE Function]\n # @BRIEF No keys → returns empty list.\n def test_list_empty(self, client, mock_db):\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n assert len(data) == 0\n # #endregion test_list_empty\n\n # #region test_list_with_keys [C:2] [TYPE Function]\n # @BRIEF Keys exist → returns list without raw_key or key_hash fields.\n def test_list_with_keys(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n # Create a key\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert len(data) == 1\n assert data[0][\"name\"] == \"Test Key\"\n assert data[0][\"prefix\"] == prefix\n assert \"raw_key\" not in data[0]\n assert \"key_hash\" not in data[0]\n # #endregion test_list_with_keys\n\n\nclass TestCreateApiKey:\n \"\"\"Contract tests for POST /api/admin/api-keys.\"\"\"\n\n # #region test_create_valid [C:2] [TYPE Function]\n # @BRIEF Valid request → 201 with raw_key, prefix, id.\n def test_create_valid(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"name\": \"CI/CD Pipeline\",\n \"permissions\": [\"maintenance:start\", \"maintenance:end\"],\n \"environment_id\": \"prod-env-1\",\n },\n )\n assert response.status_code == 201\n data = response.json()\n assert \"id\" in data\n assert \"raw_key\" in data\n assert data[\"raw_key\"].startswith(\"ssk_\")\n assert data[\"prefix\"] == data[\"raw_key\"][:11]\n assert data[\"name\"] == \"CI/CD Pipeline\"\n assert data[\"permissions\"] == [\"maintenance:start\", \"maintenance:end\"]\n assert data[\"active\"] is True\n assert data[\"environment_id\"] == \"prod-env-1\"\n # Verify raw key is NOT in DB\n from src.models.api_key import APIKey\n db_key = mock_db.query(APIKey).filter(APIKey.id == data[\"id\"]).first()\n assert db_key is not None\n assert db_key.key_hash != data[\"raw_key\"]\n assert db_key.prefix == data[\"prefix\"]\n # #endregion test_create_valid\n\n # #region test_create_missing_name [C:2] [TYPE Function]\n # @BRIEF Missing name → 400 or 422.\n def test_create_missing_name(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"permissions\": [\"maintenance:start\"],\n },\n )\n assert response.status_code in (400, 422)\n # #endregion test_create_missing_name\n\n # #region test_create_empty_permissions [C:2] [TYPE Function]\n # @BRIEF Empty permissions → 400 or 422.\n def test_create_empty_permissions(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"name\": \"No Perms\",\n \"permissions\": [],\n },\n )\n assert response.status_code in (400, 422)\n # #endregion test_create_empty_permissions\n\n\nclass TestRevokeApiKey:\n \"\"\"Contract tests for DELETE /api/admin/api-keys/{id}.\"\"\"\n\n # #region test_revoke_valid [C:2] [TYPE Function]\n # @BRIEF Revoke existing active key → 200 with status=revoked.\n def test_revoke_valid(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"To Revoke\",\n permissions=[\"maintenance:start\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == api_key.id\n assert data[\"status\"] == \"revoked\"\n\n # Verify DB\n mock_db.refresh(api_key)\n assert api_key.active is False\n # #endregion test_revoke_valid\n\n # #region test_revoke_already_revoked [C:2] [TYPE Function]\n # @BRIEF Revoke already revoked key → 404.\n def test_revoke_already_revoked(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Already Revoked\",\n permissions=[\"maintenance:start\"],\n active=False, # already revoked\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 404\n # #endregion test_revoke_already_revoked\n\n # #region test_revoke_not_found [C:2] [TYPE Function]\n # @BRIEF Revoke non-existent key → 404.\n def test_revoke_not_found(self, client, mock_db):\n response = client.delete(\"/api/admin/api-keys/nonexistent-id\")\n assert response.status_code == 404\n # #endregion test_revoke_not_found\n# #endregion TestAPIKeyRoutes\n" + "body": "# #region TestAPIKeyRoutes [C:3] [TYPE Module] [SEMANTICS test, api_key, routes, crud]\n# @BRIEF Contract tests for admin API key CRUD endpoints — list, create (raw key present), revoke, idempotent revoke.\n# @RELATION BINDS_TO -> [AdminApiKeyRoutes]\n# @TEST_CONTRACT: GET /api/admin/api-keys -> list of keys without raw_key or key_hash\n# @TEST_CONTRACT: POST /api/admin/api-keys -> 201 with raw_key in response\n# @TEST_CONTRACT: DELETE /api/admin/api-keys/{id} -> 200 with status=revoked\n# @TEST_EDGE: revoked_key_delete -> 404\n# @TEST_EDGE: missing_name -> 422\n# @TEST_EDGE: missing_permissions -> 422\nimport pytest\nimport sys\nfrom unittest.mock import MagicMock\n\nfrom fastapi.testclient import TestClient\n\n# Patch GitService at module level to prevent /app/storage/repositories error\n_mock_git_svc_cls = MagicMock()\n_mock_git_svc_cls.return_value = MagicMock()\nsys.modules['src.services.git._base'] = MagicMock(GitService=_mock_git_svc_cls)\nsys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient with all dependencies mocked.\"\"\"\n from src.app import app\n return TestClient(app)\n\n\ndef _mock_admin_auth():\n \"\"\"Apply dependency overrides for admin authentication.\"\"\"\n from src.app import app\n from src.dependencies import get_current_user\n\n mock_role = MagicMock()\n mock_role.name = \"Admin\"\n mock_role.permissions = []\n\n mock_user = MagicMock()\n mock_user.username = \"test-admin\"\n mock_user.roles = [mock_role]\n\n async def _mock_get_current_user():\n return mock_user\n\n app.dependency_overrides[get_current_user] = _mock_get_current_user\n\n\ndef _clear_overrides():\n from src.app import app\n app.dependency_overrides = {}\n\n\n@pytest.fixture(autouse=True)\ndef auth_mock():\n _mock_admin_auth()\n yield\n _clear_overrides()\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency with in-memory SQLite session.\"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.app import app\n from src.dependencies import get_db\n from src.models.mapping import Base\n\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n# ── Tests ─────────────────────────────────────────────────────\n\nclass TestListApiKeys:\n \"\"\"Contract tests for GET /api/admin/api-keys.\"\"\"\n\n # #region test_list_empty [C:2] [TYPE Function]\n # @BRIEF No keys → returns empty list.\n def test_list_empty(self, client, mock_db):\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n assert len(data) == 0\n # #endregion test_list_empty\n\n # #region test_list_with_keys [C:2] [TYPE Function]\n # @BRIEF Keys exist → returns list without raw_key or key_hash fields.\n def test_list_with_keys(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n # Create a key\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert len(data) == 1\n assert data[0][\"name\"] == \"Test Key\"\n assert data[0][\"prefix\"] == prefix\n assert \"raw_key\" not in data[0]\n assert \"key_hash\" not in data[0]\n # #endregion test_list_with_keys\n\n\nclass TestCreateApiKey:\n \"\"\"Contract tests for POST /api/admin/api-keys.\"\"\"\n\n # #region test_create_valid [C:2] [TYPE Function]\n # @BRIEF Valid request → 201 with raw_key, prefix, id.\n def test_create_valid(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"name\": \"CI/CD Pipeline\",\n \"permissions\": [\"maintenance:start\", \"maintenance:end\"],\n \"environment_id\": \"prod-env-1\",\n },\n )\n assert response.status_code == 201\n data = response.json()\n assert \"id\" in data\n assert \"raw_key\" in data\n assert data[\"raw_key\"].startswith(\"ssk_\")\n assert data[\"prefix\"] == data[\"raw_key\"][:11]\n assert data[\"name\"] == \"CI/CD Pipeline\"\n assert data[\"permissions\"] == [\"maintenance:start\", \"maintenance:end\"]\n assert data[\"active\"] is True\n assert data[\"environment_id\"] == \"prod-env-1\"\n # Verify raw key is NOT in DB\n from src.models.api_key import APIKey\n db_key = mock_db.query(APIKey).filter(APIKey.id == data[\"id\"]).first()\n assert db_key is not None\n assert db_key.key_hash != data[\"raw_key\"]\n assert db_key.prefix == data[\"prefix\"]\n # #endregion test_create_valid\n\n # #region test_create_missing_name [C:2] [TYPE Function]\n # @BRIEF Missing name → 400 or 422.\n def test_create_missing_name(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"permissions\": [\"maintenance:start\"],\n },\n )\n assert response.status_code in (400, 422)\n # #endregion test_create_missing_name\n\n # #region test_create_empty_permissions [C:2] [TYPE Function]\n # @BRIEF Empty permissions → 400 or 422.\n def test_create_empty_permissions(self, client, mock_db):\n response = client.post(\n \"/api/admin/api-keys/\",\n json={\n \"name\": \"No Perms\",\n \"permissions\": [],\n },\n )\n assert response.status_code in (400, 422)\n # #endregion test_create_empty_permissions\n\n\nclass TestRevokeApiKey:\n \"\"\"Contract tests for DELETE /api/admin/api-keys/{id}.\"\"\"\n\n # #region test_revoke_valid [C:2] [TYPE Function]\n # @BRIEF Revoke existing active key → 200 with status=revoked.\n def test_revoke_valid(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"To Revoke\",\n permissions=[\"maintenance:start\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == api_key.id\n assert data[\"status\"] == \"revoked\"\n\n # Verify DB\n mock_db.refresh(api_key)\n assert api_key.active is False\n # #endregion test_revoke_valid\n\n # #region test_revoke_already_revoked [C:2] [TYPE Function]\n # @BRIEF Revoke already revoked key → 404.\n def test_revoke_already_revoked(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Already Revoked\",\n permissions=[\"maintenance:start\"],\n active=False, # already revoked\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 404\n # #endregion test_revoke_already_revoked\n\n # #region test_revoke_not_found [C:2] [TYPE Function]\n # @BRIEF Revoke non-existent key → 404.\n def test_revoke_not_found(self, client, mock_db):\n response = client.delete(\"/api/admin/api-keys/nonexistent-id\")\n assert response.status_code == 404\n # #endregion test_revoke_not_found\n# #endregion TestAPIKeyRoutes\n" }, { "contract_id": "test_list_empty", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 103, - "end_line": 111, + "start_line": 101, + "end_line": 109, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52582,8 +53548,8 @@ "contract_id": "test_list_with_keys", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 113, - "end_line": 139, + "start_line": 111, + "end_line": 137, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52594,14 +53560,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_list_with_keys [C:2] [TYPE Function]\n # @BRIEF Keys exist → returns list without raw_key or key_hash fields.\n def test_list_with_keys(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n # Create a key\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert len(data) == 1\n assert data[0][\"name\"] == \"Test Key\"\n assert data[0][\"prefix\"] == prefix\n assert \"raw_key\" not in data[0]\n assert \"key_hash\" not in data[0]\n # #endregion test_list_with_keys\n" + "body": " # #region test_list_with_keys [C:2] [TYPE Function]\n # @BRIEF Keys exist → returns list without raw_key or key_hash fields.\n def test_list_with_keys(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n # Create a key\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.get(\"/api/admin/api-keys/\")\n assert response.status_code == 200\n data = response.json()\n assert len(data) == 1\n assert data[0][\"name\"] == \"Test Key\"\n assert data[0][\"prefix\"] == prefix\n assert \"raw_key\" not in data[0]\n assert \"key_hash\" not in data[0]\n # #endregion test_list_with_keys\n" }, { "contract_id": "test_create_valid", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 145, - "end_line": 172, + "start_line": 143, + "end_line": 170, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52618,8 +53584,8 @@ "contract_id": "test_create_missing_name", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 174, - "end_line": 184, + "start_line": 172, + "end_line": 182, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52636,8 +53602,8 @@ "contract_id": "test_create_empty_permissions", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 186, - "end_line": 197, + "start_line": 184, + "end_line": 195, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52654,8 +53620,8 @@ "contract_id": "test_revoke_valid", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 203, - "end_line": 229, + "start_line": 201, + "end_line": 227, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52666,14 +53632,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_revoke_valid [C:2] [TYPE Function]\n # @BRIEF Revoke existing active key → 200 with status=revoked.\n def test_revoke_valid(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"To Revoke\",\n permissions=[\"maintenance:start\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == api_key.id\n assert data[\"status\"] == \"revoked\"\n\n # Verify DB\n mock_db.refresh(api_key)\n assert api_key.active is False\n # #endregion test_revoke_valid\n" + "body": " # #region test_revoke_valid [C:2] [TYPE Function]\n # @BRIEF Revoke existing active key → 200 with status=revoked.\n def test_revoke_valid(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"To Revoke\",\n permissions=[\"maintenance:start\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 200\n data = response.json()\n assert data[\"id\"] == api_key.id\n assert data[\"status\"] == \"revoked\"\n\n # Verify DB\n mock_db.refresh(api_key)\n assert api_key.active is False\n # #endregion test_revoke_valid\n" }, { "contract_id": "test_revoke_already_revoked", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 231, - "end_line": 250, + "start_line": 229, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52684,14 +53650,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_revoke_already_revoked [C:2] [TYPE Function]\n # @BRIEF Revoke already revoked key → 404.\n def test_revoke_already_revoked(self, client, mock_db):\n from src.models.api_key import APIKey\n from src.core.auth.api_key import generate_api_key\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Already Revoked\",\n permissions=[\"maintenance:start\"],\n active=False, # already revoked\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 404\n # #endregion test_revoke_already_revoked\n" + "body": " # #region test_revoke_already_revoked [C:2] [TYPE Function]\n # @BRIEF Revoke already revoked key → 404.\n def test_revoke_already_revoked(self, client, mock_db):\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Already Revoked\",\n permissions=[\"maintenance:start\"],\n active=False, # already revoked\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n response = client.delete(f\"/api/admin/api-keys/{api_key.id}\")\n assert response.status_code == 404\n # #endregion test_revoke_already_revoked\n" }, { "contract_id": "test_revoke_not_found", "contract_type": "Function", "file_path": "backend/tests/test_api_key_routes.py", - "start_line": 252, - "end_line": 257, + "start_line": 250, + "end_line": 255, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52709,7 +53675,7 @@ "contract_type": "Module", "file_path": "backend/tests/test_auth.py", "start_line": 1, - "end_line": 332, + "end_line": 333, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52746,14 +53712,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestAuth [C:2] [TYPE Module]\n# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.\n# @LAYER Tests\n# @RELATION BINDS_TO -> AuthService\n# @RELATION BINDS_TO -> AuthRepository\n# @RELATION BINDS_TO -> create_admin\n# @RELATION BINDS_TO -> ensure_encryption_key\n\nimport sys\nfrom pathlib import Path\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\nfrom cryptography.fernet import Fernet\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.auth.security import get_password_hash, verify_password\nfrom src.core.database import Base\nfrom src.models.auth import ADGroupMapping, Permission, Role, User\nfrom src.scripts.create_admin import create_admin\nfrom src.scripts.init_auth_db import ensure_encryption_key\nfrom src.services.auth_service import AuthService\n\n# Create in-memory SQLite database for testing\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\n\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n# Create all tables\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create a new database session with a transaction, rollback after test\"\"\"\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef auth_service(db_session):\n return AuthService(db_session)\n\n\n@pytest.fixture\ndef auth_repo(db_session):\n return AuthRepository(db_session)\n\n\n# #region test_create_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n\n\n# #endregion test_create_user\n\n\n# #region test_authenticate_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n\n\n# #endregion test_authenticate_user\n\n\n# #region test_create_session [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n\n\n# #endregion test_create_session\n\n\n# #region test_role_permission_association [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n\n role.permissions.extend([perm1, perm2])\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n\n\n# #endregion test_role_permission_association\n\n\n# #region test_user_role_association [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n\n user.roles.append(role)\n\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n\n\n# #endregion test_user_role_association\n\n\n# #region test_ad_group_mapping [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n\n\n# #endregion test_ad_group_mapping\n\n\n# #region test_create_admin_creates_user_with_optional_email [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_admin_creates_user_with_optional_email(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation stores optional email and Admin role\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n result = create_admin(\"bootstrap-admin\", \"bootstrap-pass\", \"admin@example.com\")\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin\").first()\n )\n assert result == \"created\"\n assert created_user is not None\n assert created_user.email == \"admin@example.com\"\n assert created_user.roles[0].name == \"Admin\"\n\n\n# #endregion test_create_admin_creates_user_with_optional_email\n\n\n# #region test_create_admin_is_idempotent_for_existing_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_admin_is_idempotent_for_existing_user(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation preserves existing user on repeated runs\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n first_result = create_admin(\"bootstrap-admin-2\", \"bootstrap-pass\")\n second_result = create_admin(\n \"bootstrap-admin-2\", \"new-password\", \"changed@example.com\"\n )\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin-2\").first()\n )\n assert first_result == \"created\"\n assert second_result == \"exists\"\n assert created_user is not None\n assert created_user.email is None\n assert verify_password(\"bootstrap-pass\", created_user.password_hash)\n assert not verify_password(\"new-password\", created_user.password_hash)\n\n\n# #endregion test_create_admin_is_idempotent_for_existing_user\n\n\n# #region test_ensure_encryption_key_generates_backend_env_file [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_generates_backend_env_file(monkeypatch, tmp_path):\n \"\"\"Test first-time initialization generates and persists a Fernet key.\"\"\"\n env_file = tmp_path / \".env\"\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n generated_key = ensure_encryption_key(env_file)\n\n assert generated_key\n assert env_file.exists()\n assert (\n env_file.read_text(encoding=\"utf-8\").strip()\n == f\"ENCRYPTION_KEY={generated_key}\"\n )\n assert verify_fernet_key(generated_key)\n\n\n# #endregion test_ensure_encryption_key_generates_backend_env_file\n\n\n# #region test_ensure_encryption_key_reuses_existing_env_file_value [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_reuses_existing_env_file_value(monkeypatch, tmp_path):\n \"\"\"Test persisted key is reused without rewriting file contents.\"\"\"\n env_file = tmp_path / \".env\"\n existing_key = Fernet.generate_key().decode()\n env_file.write_text(\n f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\", encoding=\"utf-8\"\n )\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n reused_key = ensure_encryption_key(env_file)\n\n assert reused_key == existing_key\n assert (\n env_file.read_text(encoding=\"utf-8\")\n == f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\"\n )\n\n\n# #endregion test_ensure_encryption_key_reuses_existing_env_file_value\n\n\n# #region test_ensure_encryption_key_prefers_process_environment [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_prefers_process_environment(monkeypatch, tmp_path):\n \"\"\"Test explicit process environment has priority over file generation.\"\"\"\n env_file = tmp_path / \".env\"\n runtime_key = Fernet.generate_key().decode()\n monkeypatch.setenv(\"ENCRYPTION_KEY\", runtime_key)\n\n resolved_key = ensure_encryption_key(env_file)\n\n assert resolved_key == runtime_key\n assert not env_file.exists()\n\n\n# #endregion test_ensure_encryption_key_prefers_process_environment\n\n\ndef verify_fernet_key(value: str) -> bool:\n Fernet(value.encode())\n return True\n\n\n# #endregion TestAuth\n" + "body": "# #region TestAuth [C:2] [TYPE Module]\n# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.\n# @LAYER Tests\n# @RELATION BINDS_TO -> AuthService\n# @RELATION BINDS_TO -> AuthRepository\n# @RELATION BINDS_TO -> create_admin\n# @RELATION BINDS_TO -> ensure_encryption_key\n\nfrom pathlib import Path\nimport sys\n\n# Add src to path\nsys.path.append(str(Path(__file__).parent.parent / \"src\"))\n\nimport pytest\n\nfrom cryptography.fernet import Fernet\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.auth.repository import AuthRepository\nfrom src.core.auth.security import get_password_hash, verify_password\nfrom src.core.database import Base\nfrom src.models.auth import ADGroupMapping, Permission, Role, User\nfrom src.scripts.create_admin import create_admin\nfrom src.scripts.init_auth_db import ensure_encryption_key\nfrom src.services.auth_service import AuthService\n\n# Create in-memory SQLite database for testing\nSQLALCHEMY_DATABASE_URL = \"sqlite:///:memory:\"\n\nengine = create_engine(\n SQLALCHEMY_DATABASE_URL, connect_args={\"check_same_thread\": False}\n)\nTestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n# Create all tables\nBase.metadata.create_all(bind=engine)\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create a new database session with a transaction, rollback after test\"\"\"\n connection = engine.connect()\n transaction = connection.begin()\n session = TestingSessionLocal(bind=connection)\n\n yield session\n\n session.close()\n transaction.rollback()\n connection.close()\n\n\n@pytest.fixture\ndef auth_service(db_session):\n return AuthService(db_session)\n\n\n@pytest.fixture\ndef auth_repo(db_session):\n return AuthRepository(db_session)\n\n\n# #region test_create_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_user(auth_repo):\n \"\"\"Test user creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"testuser\")\n assert retrieved_user is not None\n assert retrieved_user.username == \"testuser\"\n assert retrieved_user.email == \"test@example.com\"\n assert verify_password(\"testpassword123\", retrieved_user.password_hash)\n\n\n# #endregion test_create_user\n\n\n# #region test_authenticate_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_authenticate_user(auth_service, auth_repo):\n \"\"\"Test user authentication with valid and invalid credentials\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n # Test valid credentials\n authenticated_user = auth_service.authenticate_user(\"testuser\", \"testpassword123\")\n assert authenticated_user is not None\n assert authenticated_user.username == \"testuser\"\n\n # Test invalid password\n invalid_user = auth_service.authenticate_user(\"testuser\", \"wrongpassword\")\n assert invalid_user is None\n\n # Test invalid username\n invalid_user = auth_service.authenticate_user(\"nonexistent\", \"testpassword123\")\n assert invalid_user is None\n\n\n# #endregion test_authenticate_user\n\n\n# #region test_create_session [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_session(auth_service, auth_repo):\n \"\"\"Test session token creation\"\"\"\n user = User(\n username=\"testuser\",\n email=\"test@example.com\",\n password_hash=get_password_hash(\"testpassword123\"),\n auth_source=\"LOCAL\",\n )\n\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n session = auth_service.create_session(user)\n assert \"access_token\" in session\n assert \"token_type\" in session\n assert session[\"token_type\"] == \"bearer\"\n assert len(session[\"access_token\"]) > 0\n\n\n# #endregion test_create_session\n\n\n# #region test_role_permission_association [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_role_permission_association(auth_repo):\n \"\"\"Test role and permission association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n perm1 = Permission(resource=\"admin:users\", action=\"READ\")\n perm2 = Permission(resource=\"admin:users\", action=\"WRITE\")\n\n role.permissions.extend([perm1, perm2])\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n retrieved_role = auth_repo.get_role_by_name(\"Admin\")\n assert retrieved_role is not None\n assert len(retrieved_role.permissions) == 2\n\n permissions = [f\"{p.resource}:{p.action}\" for p in retrieved_role.permissions]\n assert \"admin:users:READ\" in permissions\n assert \"admin:users:WRITE\" in permissions\n\n\n# #endregion test_role_permission_association\n\n\n# #region test_user_role_association [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_user_role_association(auth_repo):\n \"\"\"Test user and role association\"\"\"\n role = Role(name=\"Admin\", description=\"System administrator\")\n user = User(\n username=\"adminuser\",\n email=\"admin@example.com\",\n password_hash=get_password_hash(\"adminpass123\"),\n auth_source=\"LOCAL\",\n )\n\n user.roles.append(role)\n\n auth_repo.db.add(role)\n auth_repo.db.add(user)\n auth_repo.db.commit()\n\n retrieved_user = auth_repo.get_user_by_username(\"adminuser\")\n assert retrieved_user is not None\n assert len(retrieved_user.roles) == 1\n assert retrieved_user.roles[0].name == \"Admin\"\n\n\n# #endregion test_user_role_association\n\n\n# #region test_ad_group_mapping [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ad_group_mapping(auth_repo):\n \"\"\"Test AD group mapping\"\"\"\n role = Role(name=\"ADFS_Admin\", description=\"ADFS administrators\")\n\n auth_repo.db.add(role)\n auth_repo.db.commit()\n\n mapping = ADGroupMapping(ad_group=\"DOMAIN\\\\ADFS_Admins\", role_id=role.id)\n\n auth_repo.db.add(mapping)\n auth_repo.db.commit()\n\n retrieved_mapping = (\n auth_repo.db.query(ADGroupMapping)\n .filter_by(ad_group=\"DOMAIN\\\\ADFS_Admins\")\n .first()\n )\n assert retrieved_mapping is not None\n assert retrieved_mapping.role_id == role.id\n\n\n# #endregion test_ad_group_mapping\n\n\n# #region test_create_admin_creates_user_with_optional_email [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_admin_creates_user_with_optional_email(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation stores optional email and Admin role\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n result = create_admin(\"bootstrap-admin\", \"bootstrap-pass\", \"admin@example.com\")\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin\").first()\n )\n assert result == \"created\"\n assert created_user is not None\n assert created_user.email == \"admin@example.com\"\n assert created_user.roles[0].name == \"Admin\"\n\n\n# #endregion test_create_admin_creates_user_with_optional_email\n\n\n# #region test_create_admin_is_idempotent_for_existing_user [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_create_admin_is_idempotent_for_existing_user(monkeypatch, db_session):\n \"\"\"Test bootstrap admin creation preserves existing user on repeated runs\"\"\"\n monkeypatch.setattr(\"src.scripts.create_admin.AuthSessionLocal\", lambda: db_session)\n\n first_result = create_admin(\"bootstrap-admin-2\", \"bootstrap-pass\")\n second_result = create_admin(\n \"bootstrap-admin-2\", \"new-password\", \"changed@example.com\"\n )\n\n created_user = (\n db_session.query(User).filter(User.username == \"bootstrap-admin-2\").first()\n )\n assert first_result == \"created\"\n assert second_result == \"exists\"\n assert created_user is not None\n assert created_user.email is None\n assert verify_password(\"bootstrap-pass\", created_user.password_hash)\n assert not verify_password(\"new-password\", created_user.password_hash)\n\n\n# #endregion test_create_admin_is_idempotent_for_existing_user\n\n\n# #region test_ensure_encryption_key_generates_backend_env_file [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_generates_backend_env_file(monkeypatch, tmp_path):\n \"\"\"Test first-time initialization generates and persists a Fernet key.\"\"\"\n env_file = tmp_path / \".env\"\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n generated_key = ensure_encryption_key(env_file)\n\n assert generated_key\n assert env_file.exists()\n assert (\n env_file.read_text(encoding=\"utf-8\").strip()\n == f\"ENCRYPTION_KEY={generated_key}\"\n )\n assert verify_fernet_key(generated_key)\n\n\n# #endregion test_ensure_encryption_key_generates_backend_env_file\n\n\n# #region test_ensure_encryption_key_reuses_existing_env_file_value [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_reuses_existing_env_file_value(monkeypatch, tmp_path):\n \"\"\"Test persisted key is reused without rewriting file contents.\"\"\"\n env_file = tmp_path / \".env\"\n existing_key = Fernet.generate_key().decode()\n env_file.write_text(\n f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\", encoding=\"utf-8\"\n )\n monkeypatch.delenv(\"ENCRYPTION_KEY\", raising=False)\n\n reused_key = ensure_encryption_key(env_file)\n\n assert reused_key == existing_key\n assert (\n env_file.read_text(encoding=\"utf-8\")\n == f\"ENCRYPTION_KEY={existing_key}\\nOTHER=value\\n\"\n )\n\n\n# #endregion test_ensure_encryption_key_reuses_existing_env_file_value\n\n\n# #region test_ensure_encryption_key_prefers_process_environment [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestAuth\ndef test_ensure_encryption_key_prefers_process_environment(monkeypatch, tmp_path):\n \"\"\"Test explicit process environment has priority over file generation.\"\"\"\n env_file = tmp_path / \".env\"\n runtime_key = Fernet.generate_key().decode()\n monkeypatch.setenv(\"ENCRYPTION_KEY\", runtime_key)\n\n resolved_key = ensure_encryption_key(env_file)\n\n assert resolved_key == runtime_key\n assert not env_file.exists()\n\n\n# #endregion test_ensure_encryption_key_prefers_process_environment\n\n\ndef verify_fernet_key(value: str) -> bool:\n Fernet(value.encode())\n return True\n\n\n# #endregion TestAuth\n" }, { "contract_id": "test_create_admin_creates_user_with_optional_email", "contract_type": "Function", "file_path": "backend/tests/test_auth.py", - "start_line": 221, - "end_line": 238, + "start_line": 222, + "end_line": 239, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52776,8 +53742,8 @@ "contract_id": "test_create_admin_is_idempotent_for_existing_user", "contract_type": "Function", "file_path": "backend/tests/test_auth.py", - "start_line": 241, - "end_line": 263, + "start_line": 242, + "end_line": 264, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52800,8 +53766,8 @@ "contract_id": "test_ensure_encryption_key_generates_backend_env_file", "contract_type": "Function", "file_path": "backend/tests/test_auth.py", - "start_line": 266, - "end_line": 284, + "start_line": 267, + "end_line": 285, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52824,8 +53790,8 @@ "contract_id": "test_ensure_encryption_key_reuses_existing_env_file_value", "contract_type": "Function", "file_path": "backend/tests/test_auth.py", - "start_line": 287, - "end_line": 307, + "start_line": 288, + "end_line": 308, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52848,8 +53814,8 @@ "contract_id": "test_ensure_encryption_key_prefers_process_environment", "contract_type": "Function", "file_path": "backend/tests/test_auth.py", - "start_line": 310, - "end_line": 324, + "start_line": 311, + "end_line": 325, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52873,7 +53839,7 @@ "contract_type": "Module", "file_path": "backend/tests/test_constants_audit_fixes.py", "start_line": 1, - "end_line": 160, + "end_line": 158, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -52910,14 +53876,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestConstantsAuditFixes [C:3] [TYPE Module] [SEMANTICS test,constants,hardcoded,audit]\n# @BRIEF Verify Class 5 fix — hardcoded values extracted to named module-level constants.\n# @RELATION BINDS_TO -> [SupersetClientBase]\n# @RELATION BINDS_TO -> [LLMAnalysisService]\n# @RELATION BINDS_TO -> [DatabaseModule]\n# @RELATION BINDS_TO -> [GitServiceBase]\n# @TEST_EDGE: page_size_constant -> DEFAULT_PAGE_SIZE should be importable (RATIONALE says extracted)\n# @TEST_EDGE: timeout_constants -> Playwright/HTTP timeout constants should be defined (RATIONALE says extracted)\n# @TEST_EDGE: base_dir_constant -> BASE_DIR equivalent resolves to backend/ root\n# @TEST_EDGE: git_typo_fix -> services/git/_base.py still has \"repositorys\" at line 148 (bug)\nimport os\nimport sys\nfrom pathlib import Path\n\nimport pytest\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestConstantsAuditFixes:\n \"\"\"Verify Class 5 fix — named constants replace hardcoded magic values.\"\"\"\n\n # #region test_page_size_constant_exists [C:2] [TYPE Function]\n # @BRIEF DEFAULT_PAGE_SIZE contract: importable, int > 0 (not yet defined in source).\n def test_page_size_constant_exists(self):\n \"\"\"DEFAULT_PAGE_SIZE should be defined per RATIONALE comment.\n\n The RATIONALE in _base.py says 'Extracted magic number 100 into\n DEFAULT_PAGE_SIZE constant at module level' but the constant\n is NOT defined in the current source.\n \"\"\"\n try:\n from src.core.superset_client._base import DEFAULT_PAGE_SIZE\n assert isinstance(DEFAULT_PAGE_SIZE, int)\n assert DEFAULT_PAGE_SIZE > 0\n except ImportError:\n pytest.skip(\n \"DEFAULT_PAGE_SIZE is not defined in src.core.superset_client._base. \"\n \"RATIONALE comment says it was extracted but the constant is missing. \"\n \"Define as: DEFAULT_PAGE_SIZE: int = 100\"\n )\n # #endregion test_page_size_constant_exists\n\n # #region test_llm_analysis_timeout_constants [C:2] [TYPE Function]\n # @BRIEF Named timeout constants should exist per RATIONALE comment (not yet defined).\n def test_llm_analysis_timeout_constants(self):\n \"\"\"Timeout constants should be defined per RATIONALE comment.\n\n The RATIONALE says they were extracted as 'named module-level constants'\n but they don't exist in the current source.\n \"\"\"\n try:\n import importlib\n mod = importlib.import_module(\"src.plugins.llm_analysis.service\")\n except Exception:\n pytest.skip(\n \"Cannot import llm_analysis.service (transitive dependency on AuthConfig \"\n \"which crashes due to pydantic-settings v2 incompatibility)\"\n )\n return\n\n expected = [\n \"PLAYWRIGHT_NAVIGATION_TIMEOUT_MS\",\n \"PLAYWRIGHT_WAIT_TIMEOUT_MS\",\n \"PLAYWRIGHT_SHORT_TIMEOUT_MS\",\n \"HTTP_REQUEST_TIMEOUT_MS\",\n \"SCREENSHOT_SERVICE_TIMEOUT_MS\",\n \"LLM_HTTP_TIMEOUT_S\",\n ]\n missing = [c for c in expected if not hasattr(mod, c)]\n assert not missing, (\n f\"Missing timeout constants: {missing}. \"\n \"RATIONALE says they were extracted.\"\n )\n # #endregion test_llm_analysis_timeout_constants\n\n # #region test_base_dir_constant_replaces_parents [C:2] [TYPE Function]\n # @BRIEF BASE_DIR resolves to backend/ root, not __file__-dependent relative path.\n def test_base_dir_constant_replaces_parents(self):\n \"\"\"Verify BASE_DIR contract (same formula as database.py).\n\n database.py defines: BASE_DIR = Path(__file__).resolve().parent.parent.parent\n where __file__ = src/core/database.py → result = backend/ root\n \"\"\"\n db_path = Path(__file__).resolve().parent.parent / \"src\" / \"core\" / \"database.py\"\n assert db_path.exists(), f\"database.py not found at {db_path}\"\n\n # Same formula: database.py's __file__.parent.parent.parent\n base_dir = db_path.resolve().parent.parent.parent\n assert isinstance(base_dir, Path)\n assert base_dir.is_absolute()\n assert (base_dir / \"src\").is_dir(), \"BASE_DIR should point to backend/ root\"\n assert (base_dir / \"src\" / \"app.py\").exists(), \"BASE_DIR should contain src/\"\n # #endregion test_base_dir_constant_replaces_parents\n\n # #region test_git_service_repositorys_typo_regression [C:2] [TYPE Function]\n # @BRIEF services/git/_base.py still has \"repositorys\" (old typo) at line 148 — known bug.\n @pytest.mark.xfail(\n reason=\"Line 148 still uses Path('repositorys') instead of Path('repositories'). \"\n \"RATIONALE says 'Fixed typo repositorys→repositories' but it was NOT applied to line 148.\",\n )\n def test_git_service_repositorys_typo_regression(self):\n \"\"\"Verify the 'repositorys' typo is fixed in git/_base.py.\n\n Line 148 has: Path(\"repositorys\") instead of Path(\"repositories\").\n Expected: Path(\"repositorys\") is no longer used in executable code.\n \"\"\"\n git_base_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"git\" / \"_base.py\"\n source = git_base_path.read_text(encoding=\"utf-8\")\n bad_lines = []\n for i, line in enumerate(source.splitlines(), 1):\n stripped = line.strip()\n if \"repositorys\" in stripped and not stripped.startswith(\"#\"):\n bad_lines.append(f\" line {i}: {stripped}\")\n assert not bad_lines, (\n f\"Hardcoded 'repositorys' found in executable code — fix not fully applied:\\n\"\n + \"\\n\".join(bad_lines)\n )\n # #endregion test_git_service_repositorys_typo_regression\n\n # #region test_git_default_repos_path_constant [C:2] [TYPE Function]\n # @BRIEF DEFAULT_GIT_REPOS_PATH should be defined per RATIONALE (not yet defined).\n def test_git_default_repos_path_constant(self):\n \"\"\"DEFAULT_GIT_REPOS_PATH should exist per RATIONALE comment.\n\n RATIONALE says it was extracted as a constant, but the source uses\n the literal string 'git_repos' in __init__ default parameter.\n \"\"\"\n try:\n from src.services.git._base import DEFAULT_GIT_REPOS_PATH\n assert isinstance(DEFAULT_GIT_REPOS_PATH, str)\n except Exception:\n pytest.skip(\n \"DEFAULT_GIT_REPOS_PATH is not defined. \"\n \"Cannot import git._base (transitive dependency on AuthConfig crash). \"\n \"Additionally, the constant was never extracted — __init__ uses literal 'git_repos'.\"\n )\n # #endregion test_git_default_repos_path_constant\n\n # #region test_superset_client_module_loads [C:2] [TYPE Function]\n # @BRIEF superset_client._base is importable (no dependency on crashing modules).\n def test_superset_client_module_loads(self):\n from src.core.superset_client._base import SupersetClientBase\n assert SupersetClientBase is not None\n # #endregion test_superset_client_module_loads\n\n # #region test_constants_are_immutable [C:2] [TYPE Function]\n # @BRIEF Module-level constants use int/str types (immutable by convention).\n def test_constants_are_immutable(self):\n \"\"\"Check that known constants in superset_client use immutable types.\"\"\"\n from src.core.superset_client._base import SupersetClientBase\n # Verify the class is defined with constants pattern\n source = Path(SupersetClientBase.__module__.replace(\".\", \"/\") + \".py\")\n src_path = Path(__file__).parent.parent / \"src\" / source\n if src_path.exists():\n content = src_path.read_text(encoding=\"utf-8\")\n # Verify PAGE_SIZE concept is documented\n assert \"page_size\" in content\n # #endregion test_constants_are_immutable\n# #endregion TestConstantsAuditFixes\n" + "body": "# #region TestConstantsAuditFixes [C:3] [TYPE Module] [SEMANTICS test,constants,hardcoded,audit]\n# @BRIEF Verify Class 5 fix — hardcoded values extracted to named module-level constants.\n# @RELATION BINDS_TO -> [SupersetClientBase]\n# @RELATION BINDS_TO -> [LLMAnalysisService]\n# @RELATION BINDS_TO -> [DatabaseModule]\n# @RELATION BINDS_TO -> [GitServiceBase]\n# @TEST_EDGE: page_size_constant -> DEFAULT_PAGE_SIZE should be importable (RATIONALE says extracted)\n# @TEST_EDGE: timeout_constants -> Playwright/HTTP timeout constants should be defined (RATIONALE says extracted)\n# @TEST_EDGE: base_dir_constant -> BASE_DIR equivalent resolves to backend/ root\n# @TEST_EDGE: git_typo_fix -> services/git/_base.py still has \"repositorys\" at line 148 (bug)\nfrom pathlib import Path\nimport pytest\nimport sys\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestConstantsAuditFixes:\n \"\"\"Verify Class 5 fix — named constants replace hardcoded magic values.\"\"\"\n\n # #region test_page_size_constant_exists [C:2] [TYPE Function]\n # @BRIEF DEFAULT_PAGE_SIZE contract: importable, int > 0 (not yet defined in source).\n def test_page_size_constant_exists(self):\n \"\"\"DEFAULT_PAGE_SIZE should be defined per RATIONALE comment.\n\n The RATIONALE in _base.py says 'Extracted magic number 100 into\n DEFAULT_PAGE_SIZE constant at module level' but the constant\n is NOT defined in the current source.\n \"\"\"\n try:\n from src.core.superset_client._base import DEFAULT_PAGE_SIZE\n assert isinstance(DEFAULT_PAGE_SIZE, int)\n assert DEFAULT_PAGE_SIZE > 0\n except ImportError:\n pytest.skip(\n \"DEFAULT_PAGE_SIZE is not defined in src.core.superset_client._base. \"\n \"RATIONALE comment says it was extracted but the constant is missing. \"\n \"Define as: DEFAULT_PAGE_SIZE: int = 100\"\n )\n # #endregion test_page_size_constant_exists\n\n # #region test_llm_analysis_timeout_constants [C:2] [TYPE Function]\n # @BRIEF Named timeout constants should exist per RATIONALE comment (not yet defined).\n def test_llm_analysis_timeout_constants(self):\n \"\"\"Timeout constants should be defined per RATIONALE comment.\n\n The RATIONALE says they were extracted as 'named module-level constants'\n but they don't exist in the current source.\n \"\"\"\n try:\n import importlib\n mod = importlib.import_module(\"src.plugins.llm_analysis.service\")\n except Exception:\n pytest.skip(\n \"Cannot import llm_analysis.service (transitive dependency on AuthConfig \"\n \"which crashes due to pydantic-settings v2 incompatibility)\"\n )\n return\n\n expected = [\n \"PLAYWRIGHT_NAVIGATION_TIMEOUT_MS\",\n \"PLAYWRIGHT_WAIT_TIMEOUT_MS\",\n \"PLAYWRIGHT_SHORT_TIMEOUT_MS\",\n \"HTTP_REQUEST_TIMEOUT_MS\",\n \"SCREENSHOT_SERVICE_TIMEOUT_MS\",\n \"LLM_HTTP_TIMEOUT_S\",\n ]\n missing = [c for c in expected if not hasattr(mod, c)]\n assert not missing, (\n f\"Missing timeout constants: {missing}. \"\n \"RATIONALE says they were extracted.\"\n )\n # #endregion test_llm_analysis_timeout_constants\n\n # #region test_base_dir_constant_replaces_parents [C:2] [TYPE Function]\n # @BRIEF BASE_DIR resolves to backend/ root, not __file__-dependent relative path.\n def test_base_dir_constant_replaces_parents(self):\n \"\"\"Verify BASE_DIR contract (same formula as database.py).\n\n database.py defines: BASE_DIR = Path(__file__).resolve().parent.parent.parent\n where __file__ = src/core/database.py → result = backend/ root\n \"\"\"\n db_path = Path(__file__).resolve().parent.parent / \"src\" / \"core\" / \"database.py\"\n assert db_path.exists(), f\"database.py not found at {db_path}\"\n\n # Same formula: database.py's __file__.parent.parent.parent\n base_dir = db_path.resolve().parent.parent.parent\n assert isinstance(base_dir, Path)\n assert base_dir.is_absolute()\n assert (base_dir / \"src\").is_dir(), \"BASE_DIR should point to backend/ root\"\n assert (base_dir / \"src\" / \"app.py\").exists(), \"BASE_DIR should contain src/\"\n # #endregion test_base_dir_constant_replaces_parents\n\n # #region test_git_service_repositorys_typo_regression [C:2] [TYPE Function]\n # @BRIEF services/git/_base.py still has \"repositorys\" (old typo) at line 148 — known bug.\n @pytest.mark.xfail(\n reason=\"Line 148 still uses Path('repositorys') instead of Path('repositories'). \"\n \"RATIONALE says 'Fixed typo repositorys→repositories' but it was NOT applied to line 148.\",\n )\n def test_git_service_repositorys_typo_regression(self):\n \"\"\"Verify the 'repositorys' typo is fixed in git/_base.py.\n\n Line 148 has: Path(\"repositorys\") instead of Path(\"repositories\").\n Expected: Path(\"repositorys\") is no longer used in executable code.\n \"\"\"\n git_base_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"git\" / \"_base.py\"\n source = git_base_path.read_text(encoding=\"utf-8\")\n bad_lines = []\n for i, line in enumerate(source.splitlines(), 1):\n stripped = line.strip()\n if \"repositorys\" in stripped and not stripped.startswith(\"#\"):\n bad_lines.append(f\" line {i}: {stripped}\")\n assert not bad_lines, (\n \"Hardcoded 'repositorys' found in executable code — fix not fully applied:\\n\"\n + \"\\n\".join(bad_lines)\n )\n # #endregion test_git_service_repositorys_typo_regression\n\n # #region test_git_default_repos_path_constant [C:2] [TYPE Function]\n # @BRIEF DEFAULT_GIT_REPOS_PATH should be defined per RATIONALE (not yet defined).\n def test_git_default_repos_path_constant(self):\n \"\"\"DEFAULT_GIT_REPOS_PATH should exist per RATIONALE comment.\n\n RATIONALE says it was extracted as a constant, but the source uses\n the literal string 'git_repos' in __init__ default parameter.\n \"\"\"\n try:\n from src.services.git._base import DEFAULT_GIT_REPOS_PATH\n assert isinstance(DEFAULT_GIT_REPOS_PATH, str)\n except Exception:\n pytest.skip(\n \"DEFAULT_GIT_REPOS_PATH is not defined. \"\n \"Cannot import git._base (transitive dependency on AuthConfig crash). \"\n \"Additionally, the constant was never extracted — __init__ uses literal 'git_repos'.\"\n )\n # #endregion test_git_default_repos_path_constant\n\n # #region test_superset_client_module_loads [C:2] [TYPE Function]\n # @BRIEF superset_client._base is importable (no dependency on crashing modules).\n def test_superset_client_module_loads(self):\n from src.core.superset_client._base import SupersetClientBase\n assert SupersetClientBase is not None\n # #endregion test_superset_client_module_loads\n\n # #region test_constants_are_immutable [C:2] [TYPE Function]\n # @BRIEF Module-level constants use int/str types (immutable by convention).\n def test_constants_are_immutable(self):\n \"\"\"Check that known constants in superset_client use immutable types.\"\"\"\n from src.core.superset_client._base import SupersetClientBase\n # Verify the class is defined with constants pattern\n source = Path(SupersetClientBase.__module__.replace(\".\", \"/\") + \".py\")\n src_path = Path(__file__).parent.parent / \"src\" / source\n if src_path.exists():\n content = src_path.read_text(encoding=\"utf-8\")\n # Verify PAGE_SIZE concept is documented\n assert \"page_size\" in content\n # #endregion test_constants_are_immutable\n# #endregion TestConstantsAuditFixes\n" }, { "contract_id": "test_page_size_constant_exists", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 23, - "end_line": 42, + "start_line": 21, + "end_line": 40, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52934,8 +53900,8 @@ "contract_id": "test_llm_analysis_timeout_constants", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 44, - "end_line": 75, + "start_line": 42, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52952,8 +53918,8 @@ "contract_id": "test_base_dir_constant_replaces_parents", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 77, - "end_line": 94, + "start_line": 75, + "end_line": 92, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52970,8 +53936,8 @@ "contract_id": "test_git_service_repositorys_typo_regression", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 96, - "end_line": 119, + "start_line": 94, + "end_line": 117, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -52982,14 +53948,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_git_service_repositorys_typo_regression [C:2] [TYPE Function]\n # @BRIEF services/git/_base.py still has \"repositorys\" (old typo) at line 148 — known bug.\n @pytest.mark.xfail(\n reason=\"Line 148 still uses Path('repositorys') instead of Path('repositories'). \"\n \"RATIONALE says 'Fixed typo repositorys→repositories' but it was NOT applied to line 148.\",\n )\n def test_git_service_repositorys_typo_regression(self):\n \"\"\"Verify the 'repositorys' typo is fixed in git/_base.py.\n\n Line 148 has: Path(\"repositorys\") instead of Path(\"repositories\").\n Expected: Path(\"repositorys\") is no longer used in executable code.\n \"\"\"\n git_base_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"git\" / \"_base.py\"\n source = git_base_path.read_text(encoding=\"utf-8\")\n bad_lines = []\n for i, line in enumerate(source.splitlines(), 1):\n stripped = line.strip()\n if \"repositorys\" in stripped and not stripped.startswith(\"#\"):\n bad_lines.append(f\" line {i}: {stripped}\")\n assert not bad_lines, (\n f\"Hardcoded 'repositorys' found in executable code — fix not fully applied:\\n\"\n + \"\\n\".join(bad_lines)\n )\n # #endregion test_git_service_repositorys_typo_regression\n" + "body": " # #region test_git_service_repositorys_typo_regression [C:2] [TYPE Function]\n # @BRIEF services/git/_base.py still has \"repositorys\" (old typo) at line 148 — known bug.\n @pytest.mark.xfail(\n reason=\"Line 148 still uses Path('repositorys') instead of Path('repositories'). \"\n \"RATIONALE says 'Fixed typo repositorys→repositories' but it was NOT applied to line 148.\",\n )\n def test_git_service_repositorys_typo_regression(self):\n \"\"\"Verify the 'repositorys' typo is fixed in git/_base.py.\n\n Line 148 has: Path(\"repositorys\") instead of Path(\"repositories\").\n Expected: Path(\"repositorys\") is no longer used in executable code.\n \"\"\"\n git_base_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"git\" / \"_base.py\"\n source = git_base_path.read_text(encoding=\"utf-8\")\n bad_lines = []\n for i, line in enumerate(source.splitlines(), 1):\n stripped = line.strip()\n if \"repositorys\" in stripped and not stripped.startswith(\"#\"):\n bad_lines.append(f\" line {i}: {stripped}\")\n assert not bad_lines, (\n \"Hardcoded 'repositorys' found in executable code — fix not fully applied:\\n\"\n + \"\\n\".join(bad_lines)\n )\n # #endregion test_git_service_repositorys_typo_regression\n" }, { "contract_id": "test_git_default_repos_path_constant", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 121, - "end_line": 138, + "start_line": 119, + "end_line": 136, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53006,8 +53972,8 @@ "contract_id": "test_superset_client_module_loads", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 140, - "end_line": 145, + "start_line": 138, + "end_line": 143, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53024,8 +53990,8 @@ "contract_id": "test_constants_are_immutable", "contract_type": "Function", "file_path": "backend/tests/test_constants_audit_fixes.py", - "start_line": 147, - "end_line": 159, + "start_line": 145, + "end_line": 157, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53317,7 +54283,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDashboardsApi [C:2] [TYPE Module]\n# @RELATION BINDS_TO ->[DashboardsApi]\n# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API\n# @LAYER Tests\n# @SEMANTICS: tests, dashboards, api, contract, remediation\nfrom datetime import UTC, datetime\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom fastapi.testclient import TestClient\n\nfrom src.api.routes.dashboards import (\n DashboardDetailResponse,\n DashboardsResponse,\n DashboardTaskHistoryResponse,\n DatabaseMappingsResponse,\n)\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_mapping_service,\n get_resource_service,\n get_task_manager,\n has_permission,\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# #region test_get_dashboards_success [C:2] [TYPE 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# #endregion test_get_dashboards_success\n\n\n# #region test_get_dashboards_with_search [C:2] [TYPE 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# #endregion test_get_dashboards_with_search\n\n\n# #region test_get_dashboards_empty [C:2] [TYPE 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# #endregion test_get_dashboards_empty\n\n\n# #region test_get_dashboards_superset_failure [C:2] [TYPE 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# #endregion test_get_dashboards_superset_failure\n\n\n# #region test_get_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboards_env_not_found\n\n\n# #region test_get_dashboards_invalid_pagination [C:2] [TYPE 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# #endregion test_get_dashboards_invalid_pagination\n\n\n# #region test_get_database_mappings_success [C:2] [TYPE 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# #endregion test_get_database_mappings_success\n\n\n# #region test_get_database_mappings_env_not_found [C:2] [TYPE 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# #endregion test_get_database_mappings_env_not_found\n\n\n# #region test_get_dashboard_detail_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\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_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_detail_success\n\n\n# #region test_get_dashboard_detail_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboard_detail_env_not_found\n\n\n# #region test_get_dashboard_tasks_history_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(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# #endregion test_get_dashboard_tasks_history_success\n\n\n# #region test_get_dashboard_tasks_history_sorting [C:2] [TYPE 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(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# #endregion test_get_dashboard_tasks_history_sorting\n\n\n# #region test_get_dashboard_thumbnail_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\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_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_thumbnail_success\n\n\n# #region test_get_dashboard_thumbnail_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboard_thumbnail_env_not_found\n\n\n# #region test_get_dashboard_thumbnail_202 [C:2] [TYPE 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._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_thumbnail_202\n\n\n# #region test_migrate_dashboards_success [C:2] [TYPE 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# #endregion test_migrate_dashboards_success\n\n\n# #region test_migrate_dashboards_pre_checks [C:2] [TYPE 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# #endregion test_migrate_dashboards_pre_checks\n\n\n# #region test_migrate_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_migrate_dashboards_env_not_found\n\n\n# #region test_backup_dashboards_success [C:2] [TYPE 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# #endregion test_backup_dashboards_success\n\n\n# #region test_backup_dashboards_pre_checks [C:2] [TYPE 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# #endregion test_backup_dashboards_pre_checks\n\n\n# #region test_backup_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_backup_dashboards_env_not_found\n\n\n# #region test_backup_dashboards_with_schedule [C:2] [TYPE 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# #endregion test_backup_dashboards_with_schedule\n\nfrom src.api.routes.dashboards._projection import _task_matches_dashboard\n\n\n# #region test_task_matches_dashboard_logic [C:2] [TYPE 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# #endregion test_task_matches_dashboard_logic\n# #endregion TestDashboardsApi\n" + "body": "# #region TestDashboardsApi [C:2] [TYPE Module]\n# @RELATION BINDS_TO ->[DashboardsApi]\n# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API\n# @LAYER Tests\n# @SEMANTICS: tests, dashboards, api, contract, remediation\nfrom datetime import UTC, datetime\nimport pytest\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nfrom fastapi.testclient import TestClient\n\nfrom src.api.routes.dashboards import (\n DashboardDetailResponse,\n DashboardsResponse,\n DashboardTaskHistoryResponse,\n DatabaseMappingsResponse,\n)\nfrom src.app import app\nfrom src.dependencies import (\n get_config_manager,\n get_current_user,\n get_mapping_service,\n get_resource_service,\n get_task_manager,\n has_permission,\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# #region test_get_dashboards_success [C:2] [TYPE 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# #endregion test_get_dashboards_success\n\n\n# #region test_get_dashboards_with_search [C:2] [TYPE 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# #endregion test_get_dashboards_with_search\n\n\n# #region test_get_dashboards_empty [C:2] [TYPE 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# #endregion test_get_dashboards_empty\n\n\n# #region test_get_dashboards_superset_failure [C:2] [TYPE 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# #endregion test_get_dashboards_superset_failure\n\n\n# #region test_get_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboards_env_not_found\n\n\n# #region test_get_dashboards_invalid_pagination [C:2] [TYPE 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# #endregion test_get_dashboards_invalid_pagination\n\n\n# #region test_get_database_mappings_success [C:2] [TYPE 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# #endregion test_get_database_mappings_success\n\n\n# #region test_get_database_mappings_env_not_found [C:2] [TYPE 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# #endregion test_get_database_mappings_env_not_found\n\n\n# #region test_get_dashboard_detail_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\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_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_detail_success\n\n\n# #region test_get_dashboard_detail_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboard_detail_env_not_found\n\n\n# #region test_get_dashboard_tasks_history_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\ndef test_get_dashboard_tasks_history_success(mock_deps):\n now = datetime.now(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# #endregion test_get_dashboard_tasks_history_success\n\n\n# #region test_get_dashboard_tasks_history_sorting [C:2] [TYPE 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(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# #endregion test_get_dashboard_tasks_history_sorting\n\n\n# #region test_get_dashboard_thumbnail_success [C:2] [TYPE Function]\n# @RELATION BINDS_TO ->[TestDashboardsApi]\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_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_thumbnail_success\n\n\n# #region test_get_dashboard_thumbnail_env_not_found [C:2] [TYPE 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# #endregion test_get_dashboard_thumbnail_env_not_found\n\n\n# #region test_get_dashboard_thumbnail_202 [C:2] [TYPE 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._detail_routes.SupersetClient\") as mock_client_cls:\n mock_env = MagicMock()\n mock_env.id = \"prod\"\n mock_env.name = \"Production\"\n mock_env.url = \"http://localhost:8088\"\n mock_env.username = \"admin\"\n mock_env.password = \"admin\"\n mock_env.verify_ssl = True\n mock_env.timeout = 30\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# #endregion test_get_dashboard_thumbnail_202\n\n\n# #region test_migrate_dashboards_success [C:2] [TYPE 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# #endregion test_migrate_dashboards_success\n\n\n# #region test_migrate_dashboards_pre_checks [C:2] [TYPE 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# #endregion test_migrate_dashboards_pre_checks\n\n\n# #region test_migrate_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_migrate_dashboards_env_not_found\n\n\n# #region test_backup_dashboards_success [C:2] [TYPE 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# #endregion test_backup_dashboards_success\n\n\n# #region test_backup_dashboards_pre_checks [C:2] [TYPE 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# #endregion test_backup_dashboards_pre_checks\n\n\n# #region test_backup_dashboards_env_not_found [C:2] [TYPE 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# #endregion test_backup_dashboards_env_not_found\n\n\n# #region test_backup_dashboards_with_schedule [C:2] [TYPE 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# #endregion test_backup_dashboards_with_schedule\n\nfrom src.api.routes.dashboards._projection import _task_matches_dashboard\n\n\n# #region test_task_matches_dashboard_logic [C:2] [TYPE 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# #endregion test_task_matches_dashboard_logic\n# #endregion TestDashboardsApi\n" }, { "contract_id": "test_get_dashboard_tasks_history_success", @@ -53516,7 +54482,7 @@ "contract_type": "Module", "file_path": "backend/tests/test_deprecation_audit_fixes.py", "start_line": 1, - "end_line": 109, + "end_line": 108, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -53536,14 +54502,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestDeprecationAuditFixes [C:3] [TYPE Module] [SEMANTICS test,deprecation,warning,audit]\n# @BRIEF Verify Class 7 fix — is_multimodal_model should emit DeprecationWarning.\n# @RELATION BINDS_TO -> [llm_prompt_templates]\n# @TEST_EDGE: deprecated_function_warns -> is_multimodal_model should emit DeprecationWarning\n# @TEST_EDGE: old_callers_not_broken -> returns correct boolean for known multimodal models\n# @TEST_EDGE: text_only_model_returns_false -> models with text-only markers return False\n# @RATIONALE The RATIONALE comment in llm_prompt_templates.py says warnings.warn(DeprecationWarning)\n# was added, but the actual function body (lines 137-167) does NOT contain the call.\n# This is a known gap — the DeprecationWarning shim was documented but not implemented.\nimport sys\nimport warnings\nfrom pathlib import Path\n\nimport pytest\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestDeprecationAuditFixes:\n \"\"\"Verify is_multimodal_model behavior and document missing DeprecationWarning shim.\"\"\"\n\n # #region test_is_multimodal_model_emits_warning [C:2] [TYPE Function]\n # @BRIEF is_multimodal_model should emit DeprecationWarning (shim not yet implemented).\n def test_is_multimodal_model_emits_warning(self):\n \"\"\"Verify is_multimodal_model emits DeprecationWarning.\n\n NOTE: The RATIONALE comment in the source says warnings.warn(DeprecationWarning)\n was added as a deprecation shim, but the function body (llm_prompt_templates.py:137-167)\n does NOT contain the call. This test documents the gap.\n \"\"\"\n from src.services.llm_prompt_templates import is_multimodal_model\n\n # Check if the source actually has the DeprecationWarning call\n import inspect\n source = inspect.getsource(is_multimodal_model)\n if \"DeprecationWarning\" not in source:\n pytest.skip(\n \"GAP: is_multimodal_model body does not contain warnings.warn(DeprecationWarning). \"\n \"The RATIONALE comment says it was added but the implementation is missing. \"\n \"Add: warnings.warn(DeprecationWarning('is_multimodal_model is deprecated, \"\n \"use db_provider.is_multimodal flag instead'), stacklevel=2)\"\n )\n\n # If the fix IS applied, verify it works\n with pytest.warns(DeprecationWarning):\n result = is_multimodal_model(\"gpt-4o\")\n assert result is True\n # #endregion test_is_multimodal_model_emits_warning\n\n # #region test_multimodal_model_returns_true [C:2] [TYPE Function]\n # @BRIEF Known multimodal models (gpt-4o, claude-3, gemini) return True.\n def test_multimodal_model_returns_true(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n multimodal_cases = [\"gpt-4o\", \"claude-3-sonnet\", \"gemini-pro-vision\", \"gpt-4.1\"]\n for model in multimodal_cases:\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(model) is True, f\"{model} should be multimodal\"\n # #endregion test_multimodal_model_returns_true\n\n # #region test_text_only_model_returns_false [C:2] [TYPE Function]\n # @BRIEF Text-only models (embedding, whisper, rerank) return False.\n def test_text_only_model_returns_false(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n text_only_cases = [\"text-embedding-3\", \"whisper-1\", \"rerank-v2\", \"tts-1\"]\n for model in text_only_cases:\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(model) is False, f\"{model} should be text-only\"\n # #endregion test_text_only_model_returns_false\n\n # #region test_empty_model_returns_false [C:2] [TYPE Function]\n # @BRIEF Empty or None model name returns False without error.\n def test_empty_model_returns_false(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(\"\") is False\n assert is_multimodal_model(\" \") is False\n # #endregion test_empty_model_returns_false\n\n # #region test_case_insensitive_matching [C:2] [TYPE Function]\n # @BRIEF Model name matching is case-insensitive.\n def test_case_insensitive_matching(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(\"GPT-4O\") is True\n assert is_multimodal_model(\"CLAUDE-3-OPUS\") is True\n # #endregion test_case_insensitive_matching\n\n # #region test_deprecation_comment_exists [C:2] [TYPE Function]\n # @BRIEF The source module documents the @DEPRECATED rationale in the region header.\n def test_deprecation_comment_exists(self):\n \"\"\"Verify the source file documents the is_multimodal_model deprecation.\n\n The @DEPRECATED tag is in the region header (line 127), not in the function body.\n inspect.getsource(function) only returns the body (lines 137-167).\n \"\"\"\n module_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"llm_prompt_templates.py\"\n source = module_path.read_text(encoding=\"utf-8\")\n assert \"DEPRECATED\" in source, \"Source must document @DEPRECATED status\"\n assert \"is_multimodal_model is deprecated\" in source.lower() or \"deprecated\" in source.lower()\n # #endregion test_deprecation_comment_exists\n# #endregion TestDeprecationAuditFixes\n" + "body": "# #region TestDeprecationAuditFixes [C:3] [TYPE Module] [SEMANTICS test,deprecation,warning,audit]\n# @BRIEF Verify Class 7 fix — is_multimodal_model should emit DeprecationWarning.\n# @RELATION BINDS_TO -> [llm_prompt_templates]\n# @TEST_EDGE: deprecated_function_warns -> is_multimodal_model should emit DeprecationWarning\n# @TEST_EDGE: old_callers_not_broken -> returns correct boolean for known multimodal models\n# @TEST_EDGE: text_only_model_returns_false -> models with text-only markers return False\n# @RATIONALE The RATIONALE comment in llm_prompt_templates.py says warnings.warn(DeprecationWarning)\n# was added, but the actual function body (lines 137-167) does NOT contain the call.\n# This is a known gap — the DeprecationWarning shim was documented but not implemented.\nfrom pathlib import Path\nimport pytest\nimport sys\nimport warnings\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestDeprecationAuditFixes:\n \"\"\"Verify is_multimodal_model behavior and document missing DeprecationWarning shim.\"\"\"\n\n # #region test_is_multimodal_model_emits_warning [C:2] [TYPE Function]\n # @BRIEF is_multimodal_model should emit DeprecationWarning (shim not yet implemented).\n def test_is_multimodal_model_emits_warning(self):\n \"\"\"Verify is_multimodal_model emits DeprecationWarning.\n\n NOTE: The RATIONALE comment in the source says warnings.warn(DeprecationWarning)\n was added as a deprecation shim, but the function body (llm_prompt_templates.py:137-167)\n does NOT contain the call. This test documents the gap.\n \"\"\"\n # Check if the source actually has the DeprecationWarning call\n import inspect\n\n from src.services.llm_prompt_templates import is_multimodal_model\n source = inspect.getsource(is_multimodal_model)\n if \"DeprecationWarning\" not in source:\n pytest.skip(\n \"GAP: is_multimodal_model body does not contain warnings.warn(DeprecationWarning). \"\n \"The RATIONALE comment says it was added but the implementation is missing. \"\n \"Add: warnings.warn(DeprecationWarning('is_multimodal_model is deprecated, \"\n \"use db_provider.is_multimodal flag instead'), stacklevel=2)\"\n )\n\n # If the fix IS applied, verify it works\n with pytest.warns(DeprecationWarning):\n result = is_multimodal_model(\"gpt-4o\")\n assert result is True\n # #endregion test_is_multimodal_model_emits_warning\n\n # #region test_multimodal_model_returns_true [C:2] [TYPE Function]\n # @BRIEF Known multimodal models (gpt-4o, claude-3, gemini) return True.\n def test_multimodal_model_returns_true(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n multimodal_cases = [\"gpt-4o\", \"claude-3-sonnet\", \"gemini-pro-vision\", \"gpt-4.1\"]\n for model in multimodal_cases:\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(model) is True, f\"{model} should be multimodal\"\n # #endregion test_multimodal_model_returns_true\n\n # #region test_text_only_model_returns_false [C:2] [TYPE Function]\n # @BRIEF Text-only models (embedding, whisper, rerank) return False.\n def test_text_only_model_returns_false(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n text_only_cases = [\"text-embedding-3\", \"whisper-1\", \"rerank-v2\", \"tts-1\"]\n for model in text_only_cases:\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(model) is False, f\"{model} should be text-only\"\n # #endregion test_text_only_model_returns_false\n\n # #region test_empty_model_returns_false [C:2] [TYPE Function]\n # @BRIEF Empty or None model name returns False without error.\n def test_empty_model_returns_false(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(\"\") is False\n assert is_multimodal_model(\" \") is False\n # #endregion test_empty_model_returns_false\n\n # #region test_case_insensitive_matching [C:2] [TYPE Function]\n # @BRIEF Model name matching is case-insensitive.\n def test_case_insensitive_matching(self):\n from src.services.llm_prompt_templates import is_multimodal_model\n\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\", DeprecationWarning)\n assert is_multimodal_model(\"GPT-4O\") is True\n assert is_multimodal_model(\"CLAUDE-3-OPUS\") is True\n # #endregion test_case_insensitive_matching\n\n # #region test_deprecation_comment_exists [C:2] [TYPE Function]\n # @BRIEF The source module documents the @DEPRECATED rationale in the region header.\n def test_deprecation_comment_exists(self):\n \"\"\"Verify the source file documents the is_multimodal_model deprecation.\n\n The @DEPRECATED tag is in the region header (line 127), not in the function body.\n inspect.getsource(function) only returns the body (lines 137-167).\n \"\"\"\n module_path = Path(__file__).parent.parent / \"src\" / \"services\" / \"llm_prompt_templates.py\"\n source = module_path.read_text(encoding=\"utf-8\")\n assert \"DEPRECATED\" in source, \"Source must document @DEPRECATED status\"\n assert \"is_multimodal_model is deprecated\" in source.lower() or \"deprecated\" in source.lower()\n # #endregion test_deprecation_comment_exists\n# #endregion TestDeprecationAuditFixes\n" }, { "contract_id": "test_is_multimodal_model_emits_warning", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 22, - "end_line": 48, + "start_line": 21, + "end_line": 47, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53554,14 +54520,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_is_multimodal_model_emits_warning [C:2] [TYPE Function]\n # @BRIEF is_multimodal_model should emit DeprecationWarning (shim not yet implemented).\n def test_is_multimodal_model_emits_warning(self):\n \"\"\"Verify is_multimodal_model emits DeprecationWarning.\n\n NOTE: The RATIONALE comment in the source says warnings.warn(DeprecationWarning)\n was added as a deprecation shim, but the function body (llm_prompt_templates.py:137-167)\n does NOT contain the call. This test documents the gap.\n \"\"\"\n from src.services.llm_prompt_templates import is_multimodal_model\n\n # Check if the source actually has the DeprecationWarning call\n import inspect\n source = inspect.getsource(is_multimodal_model)\n if \"DeprecationWarning\" not in source:\n pytest.skip(\n \"GAP: is_multimodal_model body does not contain warnings.warn(DeprecationWarning). \"\n \"The RATIONALE comment says it was added but the implementation is missing. \"\n \"Add: warnings.warn(DeprecationWarning('is_multimodal_model is deprecated, \"\n \"use db_provider.is_multimodal flag instead'), stacklevel=2)\"\n )\n\n # If the fix IS applied, verify it works\n with pytest.warns(DeprecationWarning):\n result = is_multimodal_model(\"gpt-4o\")\n assert result is True\n # #endregion test_is_multimodal_model_emits_warning\n" + "body": " # #region test_is_multimodal_model_emits_warning [C:2] [TYPE Function]\n # @BRIEF is_multimodal_model should emit DeprecationWarning (shim not yet implemented).\n def test_is_multimodal_model_emits_warning(self):\n \"\"\"Verify is_multimodal_model emits DeprecationWarning.\n\n NOTE: The RATIONALE comment in the source says warnings.warn(DeprecationWarning)\n was added as a deprecation shim, but the function body (llm_prompt_templates.py:137-167)\n does NOT contain the call. This test documents the gap.\n \"\"\"\n # Check if the source actually has the DeprecationWarning call\n import inspect\n\n from src.services.llm_prompt_templates import is_multimodal_model\n source = inspect.getsource(is_multimodal_model)\n if \"DeprecationWarning\" not in source:\n pytest.skip(\n \"GAP: is_multimodal_model body does not contain warnings.warn(DeprecationWarning). \"\n \"The RATIONALE comment says it was added but the implementation is missing. \"\n \"Add: warnings.warn(DeprecationWarning('is_multimodal_model is deprecated, \"\n \"use db_provider.is_multimodal flag instead'), stacklevel=2)\"\n )\n\n # If the fix IS applied, verify it works\n with pytest.warns(DeprecationWarning):\n result = is_multimodal_model(\"gpt-4o\")\n assert result is True\n # #endregion test_is_multimodal_model_emits_warning\n" }, { "contract_id": "test_multimodal_model_returns_true", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 50, - "end_line": 60, + "start_line": 49, + "end_line": 59, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53578,8 +54544,8 @@ "contract_id": "test_text_only_model_returns_false", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 62, - "end_line": 72, + "start_line": 61, + "end_line": 71, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53596,8 +54562,8 @@ "contract_id": "test_empty_model_returns_false", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 74, - "end_line": 83, + "start_line": 73, + "end_line": 82, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53614,8 +54580,8 @@ "contract_id": "test_case_insensitive_matching", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 85, - "end_line": 94, + "start_line": 84, + "end_line": 93, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53632,8 +54598,8 @@ "contract_id": "test_deprecation_comment_exists", "contract_type": "Function", "file_path": "backend/tests/test_deprecation_audit_fixes.py", - "start_line": 96, - "end_line": 108, + "start_line": 95, + "end_line": 107, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53651,7 +54617,7 @@ "contract_type": "TestModule", "file_path": "backend/tests/test_layout_utils.py", "start_line": 1, - "end_line": 55, + "end_line": 54, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -53662,14 +54628,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestLayoutUtils [C:3] [TYPE TestModule] [SEMANTICS test, layout, superset, height, estimation]\n# @BRIEF Contract tests for layout utility functions — _estimate_markdown_height.\n# Verifies the height estimation formula: empty → 19, short text → computed,\n# padding accounted for, and long text capped at 200.\n# @LAYER Test\n# @RELATION BINDS_TO -> [LayoutUtils]\n# @TEST_CONTRACT: _estimate_markdown_height(str) -> int in [19, 200]\n# @TEST_EDGE: empty_content -> returns minimum height (19)\n# @TEST_EDGE: html_only_content -> returns minimum height (19)\n# @TEST_EDGE: content_with_padding -> padding:12 correctly increases height\n# @TEST_EDGE: very_long_content -> capped at maximum height (200)\nimport pytest\n\nfrom src.core.superset_client._layout_utils import _estimate_markdown_height\n\n\nclass TestEstimateMarkdownHeight:\n \"\"\"Tests for _estimate_markdown_height pure function.\"\"\"\n\n # #region test_empty_content [C:2] [TYPE Function]\n # @BRIEF Empty string returns minimum height 19.\n def test_empty_content(self):\n assert _estimate_markdown_height(\"\") == 19\n # #endregion test_empty_content\n\n # #region test_single_line_text [C:2] [TYPE Function]\n # @BRIEF Short text without padding returns expected computed height.\n # 200 chars → 200//40+1 = 6 lines → 6*22 = 132px → 40+132 = 172px → 172//8 = 21\n def test_single_line_text(self):\n text = \"A\" * 200\n assert _estimate_markdown_height(text) == 21\n # #endregion test_single_line_text\n\n # #region test_content_with_padding [C:2] [TYPE Function]\n # @BRIEF Content with padding:12 — 24px added to content height.\n # 200 chars + padding:12 → 6*22 + 24 = 156 → 40+156 = 196 → 196//8 = 24\n def test_content_with_padding(self):\n content = '
' + \"A\" * 200 + \"
\"\n assert _estimate_markdown_height(content) == 24\n # #endregion test_content_with_padding\n\n # #region test_very_long_content [C:2] [TYPE Function]\n # @BRIEF Very long content (3000 chars) capped at max height 200.\n def test_very_long_content(self):\n text = \"A\" * 3000\n assert _estimate_markdown_height(text) == 200\n # #endregion test_very_long_content\n\n # #region test_html_only_content [C:2] [TYPE Function]\n # @BRIEF HTML-only content (no visible text) returns minimum height 19.\n def test_html_only_content(self):\n content = \"

\"\n assert _estimate_markdown_height(content) == 19\n # #endregion test_html_only_content\n# #endregion TestLayoutUtils\n" + "body": "# #region TestLayoutUtils [C:3] [TYPE TestModule] [SEMANTICS test, layout, superset, height, estimation]\n# @BRIEF Contract tests for layout utility functions — _estimate_markdown_height.\n# Verifies the height estimation formula: empty → 19, short text → computed,\n# padding accounted for, and long text capped at 200.\n# @LAYER Test\n# @RELATION BINDS_TO -> [LayoutUtils]\n# @TEST_CONTRACT: _estimate_markdown_height(str) -> int in [19, 200]\n# @TEST_EDGE: empty_content -> returns minimum height (19)\n# @TEST_EDGE: html_only_content -> returns minimum height (19)\n# @TEST_EDGE: content_with_padding -> padding:12 correctly increases height\n# @TEST_EDGE: very_long_content -> capped at maximum height (200)\n\nfrom src.core.superset_client._layout_utils import _estimate_markdown_height\n\n\nclass TestEstimateMarkdownHeight:\n \"\"\"Tests for _estimate_markdown_height pure function.\"\"\"\n\n # #region test_empty_content [C:2] [TYPE Function]\n # @BRIEF Empty string returns minimum height 19.\n def test_empty_content(self):\n assert _estimate_markdown_height(\"\") == 19\n # #endregion test_empty_content\n\n # #region test_single_line_text [C:2] [TYPE Function]\n # @BRIEF Short text without padding returns expected computed height.\n # 200 chars → 200//40+1 = 6 lines → 6*22 = 132px → 40+132 = 172px → 172//8 = 21\n def test_single_line_text(self):\n text = \"A\" * 200\n assert _estimate_markdown_height(text) == 21\n # #endregion test_single_line_text\n\n # #region test_content_with_padding [C:2] [TYPE Function]\n # @BRIEF Content with padding:12 — 24px added to content height.\n # 200 chars + padding:12 → 6*22 + 24 = 156 → 40+156 = 196 → 196//8 = 24\n def test_content_with_padding(self):\n content = '
' + \"A\" * 200 + \"
\"\n assert _estimate_markdown_height(content) == 24\n # #endregion test_content_with_padding\n\n # #region test_very_long_content [C:2] [TYPE Function]\n # @BRIEF Very long content (3000 chars) capped at max height 200.\n def test_very_long_content(self):\n text = \"A\" * 3000\n assert _estimate_markdown_height(text) == 200\n # #endregion test_very_long_content\n\n # #region test_html_only_content [C:2] [TYPE Function]\n # @BRIEF HTML-only content (no visible text) returns minimum height 19.\n def test_html_only_content(self):\n content = \"

\"\n assert _estimate_markdown_height(content) == 19\n # #endregion test_html_only_content\n# #endregion TestLayoutUtils\n" }, { "contract_id": "test_empty_content", "contract_type": "Function", "file_path": "backend/tests/test_layout_utils.py", - "start_line": 20, - "end_line": 24, + "start_line": 19, + "end_line": 23, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53686,8 +54652,8 @@ "contract_id": "test_single_line_text", "contract_type": "Function", "file_path": "backend/tests/test_layout_utils.py", - "start_line": 26, - "end_line": 32, + "start_line": 25, + "end_line": 31, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53704,8 +54670,8 @@ "contract_id": "test_content_with_padding", "contract_type": "Function", "file_path": "backend/tests/test_layout_utils.py", - "start_line": 34, - "end_line": 40, + "start_line": 33, + "end_line": 39, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53722,8 +54688,8 @@ "contract_id": "test_very_long_content", "contract_type": "Function", "file_path": "backend/tests/test_layout_utils.py", - "start_line": 42, - "end_line": 47, + "start_line": 41, + "end_line": 46, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53740,8 +54706,8 @@ "contract_id": "test_html_only_content", "contract_type": "Function", "file_path": "backend/tests/test_layout_utils.py", - "start_line": 49, - "end_line": 54, + "start_line": 48, + "end_line": 53, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -53784,7 +54750,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_log_persistence [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: test, log, persistence, unit_test\n# @PURPOSE: Unit tests for TaskLogPersistenceService.\n# @LAYER Tests\n\nfrom datetime import datetime\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.task_manager.models import LogEntry, LogFilter\nfrom src.core.task_manager.persistence import TaskLogPersistenceService\nfrom src.models.mapping import Base\n\n\n# #region TestLogPersistence [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # #region setup_class [C:2] [TYPE Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n from sqlalchemy import event\n event.listen(cls.engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # #endregion setup_class\n\n # #region teardown_class [C:2] [TYPE Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # #endregion teardown_class\n\n # #region setup_method [C:2] [TYPE Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n from src.models.task import TaskLogRecord, TaskRecord\n session = self.TestSessionLocal()\n # Create FK parent records for all task_ids used in tests\n for tid in ['test-task-1', 'test-task-2', 'test-task-3', 'test-task-4',\n 'test-task-5', 'test-task-6', 'test-task-7', 'test-task-8',\n 'test-task-9', 'multi-1', 'multi-2', 'multi-3']:\n existing = session.query(TaskRecord).filter(TaskRecord.id == tid).first()\n if not existing:\n session.add(TaskRecord(id=tid, type='test', status='PENDING'))\n session.commit()\n # Clean task_logs table\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # #endregion setup_method\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n\n # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n\n # #region test_add_logs_empty [C:2] [TYPE Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # #endregion test_add_logs_empty\n\n # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n\n # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n\n # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n\n # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n\n # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n\n # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n\n # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n\n # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n\n # #region test_delete_logs_for_tasks_empty [C:2] [TYPE Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # #endregion test_delete_logs_for_tasks_empty\n\n# #endregion TestLogPersistence\n# #endregion test_log_persistence\n" + "body": "# #region test_log_persistence [C:2] [TYPE Module]\n# @RELATION BINDS_TO -> SrcRoot\n# @SEMANTICS: test, log, persistence, unit_test\n# @PURPOSE: Unit tests for TaskLogPersistenceService.\n# @LAYER Tests\n\nfrom datetime import UTC, datetime\nfrom unittest.mock import patch\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom src.core.task_manager.models import LogEntry, LogFilter\nfrom src.core.task_manager.persistence import TaskLogPersistenceService\nfrom src.models.mapping import Base\n\n\n# #region TestLogPersistence [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # #region setup_class [C:2] [TYPE Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n from sqlalchemy import event\n event.listen(cls.engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # #endregion setup_class\n\n # #region teardown_class [C:2] [TYPE Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # #endregion teardown_class\n\n # #region setup_method [C:2] [TYPE Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n from src.models.task import TaskLogRecord, TaskRecord\n session = self.TestSessionLocal()\n # Create FK parent records for all task_ids used in tests\n for tid in ['test-task-1', 'test-task-2', 'test-task-3', 'test-task-4',\n 'test-task-5', 'test-task-6', 'test-task-7', 'test-task-8',\n 'test-task-9', 'multi-1', 'multi-2', 'multi-3']:\n existing = session.query(TaskRecord).filter(TaskRecord.id == tid).first()\n if not existing:\n session.add(TaskRecord(id=tid, type='test', status='PENDING'))\n session.commit()\n # Clean task_logs table\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # #endregion setup_method\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.now(UTC),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n\n # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n\n # #region test_add_logs_empty [C:2] [TYPE Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # #endregion test_add_logs_empty\n\n # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n\n # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n\n # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n\n # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n\n # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n\n # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n\n # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n\n # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n\n # #region test_delete_logs_for_tasks_empty [C:2] [TYPE Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # #endregion test_delete_logs_for_tasks_empty\n\n# #endregion TestLogPersistence\n# #endregion test_log_persistence\n" }, { "contract_id": "TestLogPersistence", @@ -53810,7 +54776,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestLogPersistence [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # #region setup_class [C:2] [TYPE Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n from sqlalchemy import event\n event.listen(cls.engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # #endregion setup_class\n\n # #region teardown_class [C:2] [TYPE Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # #endregion teardown_class\n\n # #region setup_method [C:2] [TYPE Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n from src.models.task import TaskLogRecord, TaskRecord\n session = self.TestSessionLocal()\n # Create FK parent records for all task_ids used in tests\n for tid in ['test-task-1', 'test-task-2', 'test-task-3', 'test-task-4',\n 'test-task-5', 'test-task-6', 'test-task-7', 'test-task-8',\n 'test-task-9', 'multi-1', 'multi-2', 'multi-3']:\n existing = session.query(TaskRecord).filter(TaskRecord.id == tid).first()\n if not existing:\n session.add(TaskRecord(id=tid, type='test', status='PENDING'))\n session.commit()\n # Clean task_logs table\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # #endregion setup_method\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n\n # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n\n # #region test_add_logs_empty [C:2] [TYPE Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # #endregion test_add_logs_empty\n\n # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n\n # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n\n # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n\n # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n\n # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n\n # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n\n # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n\n # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n\n # #region test_delete_logs_for_tasks_empty [C:2] [TYPE Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # #endregion test_delete_logs_for_tasks_empty\n\n# #endregion TestLogPersistence\n" + "body": "# #region TestLogPersistence [C:2] [TYPE Class]\n# @RELATION BINDS_TO -> test_log_persistence\n# @PURPOSE: Test suite for TaskLogPersistenceService.\n# @TEST_DATA: log_entry -> {\"task_id\": \"test-task-1\", \"level\": \"INFO\", \"source\": \"test_source\", \"message\": \"Test message\"}\nclass TestLogPersistence:\n\n # #region setup_class [C:2] [TYPE Function]\n # @PURPOSE: Setup test database and service instance.\n # @PRE: None.\n # @POST: In-memory database and service instance created.\n @classmethod\n def setup_class(cls):\n \"\"\"Create an in-memory database for testing.\"\"\"\n cls.engine = create_engine(\"sqlite:///:memory:\")\n from sqlalchemy import event\n event.listen(cls.engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(bind=cls.engine)\n cls.TestSessionLocal = sessionmaker(bind=cls.engine)\n cls.service = TaskLogPersistenceService()\n # #endregion setup_class\n\n # #region teardown_class [C:2] [TYPE Function]\n # @PURPOSE: Clean up test database.\n # @PRE: None.\n # @POST: Database disposed.\n @classmethod\n def teardown_class(cls):\n \"\"\"Dispose of the database engine.\"\"\"\n cls.engine.dispose()\n # #endregion teardown_class\n\n # #region setup_method [C:2] [TYPE Function]\n # @PURPOSE: Setup for each test method — clean task_logs table.\n # @PRE: None.\n # @POST: task_logs table is empty.\n def setup_method(self):\n \"\"\"Clean task_logs table before each test.\"\"\"\n from src.models.task import TaskLogRecord, TaskRecord\n session = self.TestSessionLocal()\n # Create FK parent records for all task_ids used in tests\n for tid in ['test-task-1', 'test-task-2', 'test-task-3', 'test-task-4',\n 'test-task-5', 'test-task-6', 'test-task-7', 'test-task-8',\n 'test-task-9', 'multi-1', 'multi-2', 'multi-3']:\n existing = session.query(TaskRecord).filter(TaskRecord.id == tid).first()\n if not existing:\n session.add(TaskRecord(id=tid, type='test', status='PENDING'))\n session.commit()\n # Clean task_logs table\n session.query(TaskLogRecord).delete()\n session.commit()\n session.close()\n # #endregion setup_method\n\n def _patched(self, method_name):\n \"\"\"Helper: returns a patch context for TasksSessionLocal.\"\"\"\n return patch(\n \"src.core.task_manager.persistence.TasksSessionLocal\",\n self.TestSessionLocal\n )\n\n # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.now(UTC),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n\n # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n\n # #region test_add_logs_empty [C:2] [TYPE Function]\n # @PURPOSE: Test adding empty log list (should be no-op).\n # @PRE: Service initialized.\n # @POST: No logs added.\n def test_add_logs_empty(self):\n \"\"\"Test adding empty log list is a no-op.\"\"\"\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-X\", [])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-X\").all()\n session.close()\n assert len(results) == 0\n # #endregion test_add_logs_empty\n\n # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n\n # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n\n # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n\n # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n\n # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n\n # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n\n # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n\n # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n\n # #region test_delete_logs_for_tasks_empty [C:2] [TYPE Function]\n # @PURPOSE: Test deleting with empty list (no-op).\n # @PRE: Service initialized.\n # @POST: No error, no deletion.\n def test_delete_logs_for_tasks_empty(self):\n \"\"\"Test deleting with empty list is a no-op.\"\"\"\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([]) # Should not raise\n # #endregion test_delete_logs_for_tasks_empty\n\n# #endregion TestLogPersistence\n" }, { "contract_id": "test_add_logs_single", @@ -53830,7 +54796,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.utcnow(),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n" + "body": " # #region test_add_logs_single [C:2] [TYPE Function]\n # @PURPOSE: Test adding a single log entry.\n # @PRE: Service and session initialized.\n # @POST: Log entry persisted to database.\n def test_add_logs_single(self):\n \"\"\"Test adding a single log entry via add_logs.\"\"\"\n entry = LogEntry(\n timestamp=datetime.now(UTC),\n level=\"INFO\",\n source=\"test_source\",\n message=\"Test message\"\n )\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-1\", [entry])\n\n # Query the database\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n result = session.query(TaskLogRecord).filter_by(task_id=\"test-task-1\").first()\n session.close()\n\n assert result is not None\n assert result.level == \"INFO\"\n assert result.source == \"test_source\"\n assert result.message == \"Test message\"\n # #endregion test_add_logs_single\n" }, { "contract_id": "test_add_logs_batch", @@ -53850,7 +54816,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n" + "body": " # #region test_add_logs_batch [C:2] [TYPE Function]\n # @PURPOSE: Test adding multiple log entries in batch.\n # @PRE: Service and session initialized.\n # @POST: All log entries persisted to database.\n def test_add_logs_batch(self):\n \"\"\"Test adding multiple log entries in batch.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"source1\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"source2\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"source3\", message=\"Message 3\"),\n ]\n\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-2\", entries)\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n results = session.query(TaskLogRecord).filter_by(task_id=\"test-task-2\").all()\n session.close()\n\n assert len(results) == 3\n # #endregion test_add_logs_batch\n" }, { "contract_id": "test_add_logs_empty", @@ -53890,7 +54856,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n" + "body": " # #region test_get_logs_by_task_id [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs for the specified task.\n def test_get_logs_by_task_id(self):\n \"\"\"Test retrieving logs by task ID using LogFilter.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"src1\", message=f\"Message {i}\")\n for i in range(5)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-3\", entries)\n\n with self._patched(\"get_logs\"):\n logs = self.service.get_logs(\"test-task-3\", LogFilter())\n\n assert len(logs) == 5\n assert all(log.task_id == \"test-task-3\" for log in logs)\n # #endregion test_get_logs_by_task_id\n" }, { "contract_id": "test_get_logs_with_filters", @@ -53910,7 +54876,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n" + "body": " # #region test_get_logs_with_filters [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with level and source filters.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns filtered logs.\n def test_get_logs_with_filters(self):\n \"\"\"Test retrieving logs with level and source filters.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning message\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error message\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-4\", entries)\n\n # Test level filter\n with self._patched(\"get_logs\"):\n warning_logs = self.service.get_logs(\"test-task-4\", LogFilter(level=\"WARNING\"))\n assert len(warning_logs) == 1\n assert warning_logs[0].level == \"WARNING\"\n\n # Test source filter\n with self._patched(\"get_logs\"):\n api_logs = self.service.get_logs(\"test-task-4\", LogFilter(source=\"api\"))\n assert len(api_logs) == 2\n assert all(log.source == \"api\" for log in api_logs)\n # #endregion test_get_logs_with_filters\n" }, { "contract_id": "test_get_logs_with_pagination", @@ -53930,7 +54896,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n" + "body": " # #region test_get_logs_with_pagination [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with pagination.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns paginated logs.\n def test_get_logs_with_pagination(self):\n \"\"\"Test retrieving logs with pagination.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(15)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-5\", entries)\n\n with self._patched(\"get_logs\"):\n page1 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=0))\n assert len(page1) == 10\n\n with self._patched(\"get_logs\"):\n page2 = self.service.get_logs(\"test-task-5\", LogFilter(limit=10, offset=10))\n assert len(page2) == 5\n # #endregion test_get_logs_with_pagination\n" }, { "contract_id": "test_get_logs_with_search", @@ -53950,7 +54916,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n" + "body": " # #region test_get_logs_with_search [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving logs with search query.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns logs matching search query.\n def test_get_logs_with_search(self):\n \"\"\"Test retrieving logs with search query.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"User authentication successful\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"api\", message=\"Failed to connect to database\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"File saved successfully\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-6\", entries)\n\n with self._patched(\"get_logs\"):\n auth_logs = self.service.get_logs(\"test-task-6\", LogFilter(search=\"authentication\"))\n assert len(auth_logs) == 1\n assert \"authentication\" in auth_logs[0].message.lower()\n # #endregion test_get_logs_with_search\n" }, { "contract_id": "test_get_log_stats", @@ -53970,7 +54936,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n" + "body": " # #region test_get_log_stats [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving log statistics.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns LogStats model with counts by level and source.\n def test_get_log_stats(self):\n \"\"\"Test retrieving log statistics as LogStats model.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Info 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"WARNING\", source=\"api\", message=\"Warning 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"ERROR\", source=\"storage\", message=\"Error 1\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-7\", entries)\n\n with self._patched(\"get_log_stats\"):\n stats = self.service.get_log_stats(\"test-task-7\")\n\n assert stats is not None\n assert stats.total_count == 4\n assert stats.by_level[\"INFO\"] == 2\n assert stats.by_level[\"WARNING\"] == 1\n assert stats.by_level[\"ERROR\"] == 1\n assert stats.by_source[\"api\"] == 3\n assert stats.by_source[\"storage\"] == 1\n # #endregion test_get_log_stats\n" }, { "contract_id": "test_get_sources", @@ -53990,7 +54956,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n" + "body": " # #region test_get_sources [C:2] [TYPE Function]\n # @PURPOSE: Test retrieving unique log sources.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Returns list of unique sources.\n def test_get_sources(self):\n \"\"\"Test retrieving unique log sources.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"api\", message=\"Message 1\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"storage\", message=\"Message 2\"),\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"git\", message=\"Message 3\"),\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-8\", entries)\n\n with self._patched(\"get_sources\"):\n sources = self.service.get_sources(\"test-task-8\")\n\n assert len(sources) == 3\n assert \"api\" in sources\n assert \"storage\" in sources\n assert \"git\" in sources\n # #endregion test_get_sources\n" }, { "contract_id": "test_delete_logs_for_task", @@ -54010,7 +54976,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n" + "body": " # #region test_delete_logs_for_task [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs by task ID.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for the task are deleted.\n def test_delete_logs_for_task(self):\n \"\"\"Test deleting logs by task ID.\"\"\"\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=f\"Message {i}\")\n for i in range(3)\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(\"test-task-9\", entries)\n\n # Verify logs exist\n with self._patched(\"get_logs\"):\n logs_before = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_before) == 3\n\n # Delete logs\n with self._patched(\"delete_logs_for_task\"):\n self.service.delete_logs_for_task(\"test-task-9\")\n\n # Verify logs are deleted\n with self._patched(\"get_logs\"):\n logs_after = self.service.get_logs(\"test-task-9\", LogFilter())\n assert len(logs_after) == 0\n # #endregion test_delete_logs_for_task\n" }, { "contract_id": "test_delete_logs_for_tasks", @@ -54030,7 +54996,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.utcnow(), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n" + "body": " # #region test_delete_logs_for_tasks [C:2] [TYPE Function]\n # @PURPOSE: Test deleting logs for multiple tasks.\n # @PRE: Service and session initialized, logs exist.\n # @POST: Logs for all specified tasks are deleted.\n def test_delete_logs_for_tasks(self):\n \"\"\"Test deleting logs for multiple tasks at once.\"\"\"\n for task_id in [\"multi-1\", \"multi-2\", \"multi-3\"]:\n entries = [\n LogEntry(timestamp=datetime.now(UTC), level=\"INFO\", source=\"test\", message=\"msg\")\n ]\n with self._patched(\"add_logs\"):\n self.service.add_logs(task_id, entries)\n\n with self._patched(\"delete_logs_for_tasks\"):\n self.service.delete_logs_for_tasks([\"multi-1\", \"multi-2\"])\n\n from src.models.task import TaskLogRecord\n session = self.TestSessionLocal()\n remaining = session.query(TaskLogRecord).all()\n session.close()\n assert len(remaining) == 1\n assert remaining[0].task_id == \"multi-3\"\n # #endregion test_delete_logs_for_tasks\n" }, { "contract_id": "test_delete_logs_for_tasks_empty", @@ -54057,7 +55023,7 @@ "contract_type": "Module", "file_path": "backend/tests/test_logger.py", "start_line": 1, - "end_line": 358, + "end_line": 357, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54084,14 +55050,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestLogger [C:2] [TYPE Module]\n# @SEMANTICS: logging, tests, belief_state, cot, json\n# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.\n# @LAYER Tests\n# @RELATION BINDS_TO -> [LoggerModule]\n# @INVARIANT: All required log statements must correctly check the threshold.\n\nimport logging\n\nimport pytest\n\nfrom src.core.config_models import LoggingConfig\nfrom src.core.logger import (\n CotJsonFormatter,\n belief_scope,\n configure_logger,\n get_task_log_level,\n logger,\n should_log_task_level,\n)\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n\n# #region test_belief_scope_logs_reason_reflect_at_debug [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n\n\n# #region test_belief_scope_error_handling [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error context.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, f\"EXPLORE marker not found for FailingFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n\n\n# #region test_belief_scope_success_coherence [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, f\"REFLECT marker not found for SuccessFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# #endregion test_belief_scope_success_coherence\n\n\n# #region test_belief_scope_reason_not_visible_at_info [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# #endregion test_belief_scope_reason_not_visible_at_info\n\n\n# #region test_task_log_level_default [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO.\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# #endregion test_task_log_level_default\n\n\n# #region test_should_log_task_level [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# #endregion test_should_log_task_level\n\n\n# #region test_configure_logger_task_log_level [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n\n # Reset to INFO\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n assert get_task_log_level() == \"INFO\", \"task_log_level should be reset to INFO\"\n# #endregion test_configure_logger_task_log_level\n\n\n# #region test_enable_belief_state_flag [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n\n # Re-enable for other tests\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n# #endregion test_enable_belief_state_flag\n\n# #region test_cot_json_formatter_output [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# #endregion test_cot_json_formatter_output\n\n# #region test_cot_json_formatter_plain_message [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# #endregion test_cot_json_formatter_plain_message\n\n# #endregion TestLogger\n" + "body": "# #region TestLogger [C:2] [TYPE Module]\n# @SEMANTICS: logging, tests, belief_state, cot, json\n# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.\n# @LAYER Tests\n# @RELATION BINDS_TO -> [LoggerModule]\n# @INVARIANT: All required log statements must correctly check the threshold.\n\nimport logging\nimport pytest\n\nfrom src.core.config_models import LoggingConfig\nfrom src.core.logger import (\n CotJsonFormatter,\n belief_scope,\n configure_logger,\n get_task_log_level,\n logger,\n should_log_task_level,\n)\n\n\n@pytest.fixture(autouse=True)\ndef reset_logger_state():\n \"\"\"Reset logger state before each test to avoid cross-test contamination.\"\"\"\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n # Also reset the logger level for caplog to work correctly\n logging.getLogger(\"superset_tools_app\").setLevel(logging.DEBUG)\n yield\n # Reset after test too\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n\n# #region test_belief_scope_logs_reason_reflect_at_debug [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REASON (entry) and REFLECT (coherence) markers.\ndef test_belief_scope_logs_reason_reflect_at_debug(caplog):\n \"\"\"Test that belief_scope generates REASON and REFLECT CoT markers at DEBUG level.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"TestFunction\"):\n logger.info(\"Doing something important\")\n\n # Check that the records contain the expected markers\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'TestFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'TestFunction'\n ]\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n\n assert len(reason_records) >= 1, \"REASON marker not found for TestFunction\"\n assert len(reflect_records) >= 1, \"REFLECT marker not found for TestFunction\"\n assert len(info_records) >= 1, \"INFO log 'Doing something important' not found\"\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_logs_reason_reflect_at_debug\n\n\n# #region test_belief_scope_error_handling [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs EXPLORE marker on exception.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain EXPLORE marker with error context.\ndef test_belief_scope_error_handling(caplog):\n \"\"\"Test that belief_scope logs EXPLORE marker on exception.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with pytest.raises(ValueError), belief_scope(\"FailingFunction\"):\n raise ValueError(\"Something went wrong\")\n\n # Check that an EXPLORE marker was emitted\n explore_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'EXPLORE'\n and getattr(r, 'src', None) == 'FailingFunction'\n ]\n\n assert len(explore_records) >= 1, f\"EXPLORE marker not found for FailingFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Something went wrong' in explore_records[0].getMessage() or \\\n 'Something went wrong' in str(getattr(explore_records[0], 'error', ''))\n\n # Reset to INFO\n config = LoggingConfig(level=\"INFO\", task_log_level=\"INFO\", enable_belief_state=True)\n configure_logger(config)\n# #endregion test_belief_scope_error_handling\n\n\n# #region test_belief_scope_success_coherence [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope logs REFLECT marker on success.\n# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.\n# @POST: Logs are verified to contain REFLECT marker.\ndef test_belief_scope_success_coherence(caplog):\n \"\"\"Test that belief_scope logs REFLECT marker on success.\"\"\"\n # Configure logger to DEBUG level\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"SuccessFunction\"):\n pass\n\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT'\n and getattr(r, 'src', None) == 'SuccessFunction'\n ]\n\n assert len(reflect_records) >= 1, f\"REFLECT marker not found for SuccessFunction. Logs: {[(r.levelname, getattr(r, 'marker', ''), r.getMessage()) for r in caplog.records]}\"\n assert 'Coherence OK' in reflect_records[0].getMessage() or \\\n getattr(reflect_records[0], 'intent', '') == 'Coherence OK'\n\n\n# #endregion test_belief_scope_success_coherence\n\n\n# #region test_belief_scope_reason_not_visible_at_info [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\n# @PRE: belief_scope is available. caplog fixture is used.\n# @POST: REASON/REFLECT markers are not captured at INFO level.\ndef test_belief_scope_reason_not_visible_at_info(caplog):\n \"\"\"Test that belief_scope REASON/REFLECT markers are NOT visible at INFO level.\"\"\"\n caplog.set_level(\"INFO\")\n\n with belief_scope(\"InfoLevelFunction\"):\n logger.info(\"Doing something important\")\n\n # The REASON and REFLECT markers are debug-level, so they should NOT appear at INFO\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'InfoLevelFunction'\n ]\n\n assert len(reason_records) == 0, \"REASON marker should not be visible at INFO\"\n assert len(reflect_records) == 0, \"REFLECT marker should not be visible at INFO\"\n\n # But the INFO-level message should be visible\n info_records = [\n r for r in caplog.records\n if r.levelname == 'INFO' and 'Doing something important' in r.getMessage()\n ]\n assert len(info_records) >= 1, \"INFO log 'Doing something important' should be visible\"\n# #endregion test_belief_scope_reason_not_visible_at_info\n\n\n# #region test_task_log_level_default [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that default task log level is INFO.\n# @PRE: None.\n# @POST: Default level is INFO.\ndef test_task_log_level_default():\n \"\"\"Test that default task log level is INFO.\"\"\"\n level = get_task_log_level()\n assert level == \"INFO\"\n# #endregion test_task_log_level_default\n\n\n# #region test_should_log_task_level [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that should_log_task_level correctly filters log levels.\n# @PRE: None.\n# @POST: Filtering works correctly for all level combinations.\ndef test_should_log_task_level():\n \"\"\"Test that should_log_task_level correctly filters log levels.\"\"\"\n # Default level is INFO\n assert should_log_task_level(\"ERROR\") is True, \"ERROR should be logged at INFO threshold\"\n assert should_log_task_level(\"WARNING\") is True, \"WARNING should be logged at INFO threshold\"\n assert should_log_task_level(\"INFO\") is True, \"INFO should be logged at INFO threshold\"\n assert should_log_task_level(\"DEBUG\") is False, \"DEBUG should NOT be logged at INFO threshold\"\n# #endregion test_should_log_task_level\n\n\n# #region test_configure_logger_task_log_level [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that configure_logger updates task_log_level.\n# @PRE: LoggingConfig is available.\n# @POST: task_log_level is updated correctly.\ndef test_configure_logger_task_log_level():\n \"\"\"Test that configure_logger updates task_log_level.\"\"\"\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n\n assert get_task_log_level() == \"DEBUG\", \"task_log_level should be DEBUG\"\n assert should_log_task_level(\"DEBUG\") is True, \"DEBUG should be logged at DEBUG threshold\"\n\n # Reset to INFO\n config = LoggingConfig(\n level=\"INFO\",\n task_log_level=\"INFO\",\n enable_belief_state=True\n )\n configure_logger(config)\n assert get_task_log_level() == \"INFO\", \"task_log_level should be reset to INFO\"\n# #endregion test_configure_logger_task_log_level\n\n\n# #region test_enable_belief_state_flag [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that enable_belief_state flag controls belief_scope entry logging.\n# @PRE: LoggingConfig is available. caplog fixture is used.\n# @POST: REASON entry marker suppressed when disabled; REFLECT coherence still logged.\ndef test_enable_belief_state_flag(caplog):\n \"\"\"Test that enable_belief_state flag controls belief_scope REASON entry logging.\"\"\"\n # Disable belief state\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=False\n )\n configure_logger(config)\n\n caplog.set_level(\"DEBUG\")\n\n with belief_scope(\"DisabledFunction\"):\n logger.info(\"Doing something\")\n\n # REASON entry marker should NOT be logged when disabled\n reason_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REASON' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reason_records) == 0, \"REASON entry should not be logged when disabled\"\n\n # REFLECT coherence marker should still be logged (internal tracking)\n reflect_records = [\n r for r in caplog.records\n if getattr(r, 'marker', None) == 'REFLECT' and getattr(r, 'src', None) == 'DisabledFunction'\n ]\n assert len(reflect_records) >= 1, \"REFLECT coherence should still be logged\"\n\n # Re-enable for other tests\n config = LoggingConfig(\n level=\"DEBUG\",\n task_log_level=\"DEBUG\",\n enable_belief_state=True\n )\n configure_logger(config)\n# #endregion test_enable_belief_state_flag\n\n# #region test_cot_json_formatter_output [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter produces valid JSON with expected fields.\ndef test_cot_json_formatter_output():\n \"\"\"Test that CotJsonFormatter produces valid JSON with expected fields.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord with structured extra data\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Test action\",\n args=(),\n exc_info=None,\n )\n record.marker = \"REASON\"\n record.intent = \"Test action\"\n record.src = \"TestModule\"\n record.payload = {\"key\": \"value\"}\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\"\n assert parsed[\"intent\"] == \"Test action\"\n assert parsed[\"src\"] == \"TestModule\"\n assert parsed[\"payload\"] == {\"key\": \"value\"}\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# #endregion test_cot_json_formatter_output\n\n# #region test_cot_json_formatter_plain_message [C:2] [TYPE Function]\n# @RELATION BINDS_TO -> TestLogger\n# @PURPOSE: Test that CotJsonFormatter wraps plain messages (no extra) with default marker.\ndef test_cot_json_formatter_plain_message():\n \"\"\"Test that CotJsonFormatter wraps plain messages with default marker.\"\"\"\n import json\n\n formatter = CotJsonFormatter()\n\n # Create a LogRecord WITHOUT extra data (plain message)\n record = logging.LogRecord(\n name=\"test.module\",\n level=logging.INFO,\n pathname=\"/fake/path.py\",\n lineno=42,\n msg=\"Plain info message\",\n args=(),\n exc_info=None,\n )\n\n output = formatter.format(record)\n parsed = json.loads(output)\n\n assert parsed[\"level\"] == \"INFO\"\n assert parsed[\"marker\"] == \"REASON\" # default for plain messages\n assert parsed[\"intent\"] == \"Plain info message\"\n assert parsed[\"src\"] == \"test.module\"\n assert \"ts\" in parsed\n assert \"trace_id\" in parsed\n# #endregion test_cot_json_formatter_plain_message\n\n# #endregion TestLogger\n" }, { "contract_id": "test_belief_scope_success_coherence", "contract_type": "Function", "file_path": "backend/tests/test_logger.py", - "start_line": 125, - "end_line": 156, + "start_line": 124, + "end_line": 155, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54117,8 +55083,8 @@ "contract_id": "test_belief_scope_reason_not_visible_at_info", "contract_type": "Function", "file_path": "backend/tests/test_logger.py", - "start_line": 159, - "end_line": 190, + "start_line": 158, + "end_line": 189, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54144,8 +55110,8 @@ "contract_id": "test_cot_json_formatter_output", "contract_type": "Function", "file_path": "backend/tests/test_logger.py", - "start_line": 291, - "end_line": 325, + "start_line": 290, + "end_line": 324, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54169,8 +55135,8 @@ "contract_id": "test_cot_json_formatter_plain_message", "contract_type": "Function", "file_path": "backend/tests/test_logger.py", - "start_line": 327, - "end_line": 356, + "start_line": 326, + "end_line": 355, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54214,7 +55180,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region TestLoggingAuditFixes [C:3] [TYPE Module] [SEMANTICS test,logging,except,audit]\n# @BRIEF Verify Class 2 fix — no bare `except: pass` patterns remain in src/.\n# @RELATION BINDS_TO -> [EXT:frontend:PluginLoaderCore]\n# @TEST_EDGE: except_pass_replaced -> bare `except: pass` patterns are absent from src/\n# @TEST_EDGE: plugin_loader_uses_logger -> plugin_loader.py uses logger, not print\nimport ast\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestLoggingAuditFixes:\n \"\"\"Verify that all bare `except: pass` patterns have been replaced with proper logging.\"\"\"\n\n PLUGIN_LOADER_PATH = Path(__file__).parent.parent / \"src\" / \"core\" / \"plugin_loader.py\"\n\n # #region _collect_src_files [C:1] [TYPE Function]\n # @BRIEF Collect all .py files under src/ excluding test/ and mock/ directories.\n @staticmethod\n def _collect_src_files():\n src_root = Path(__file__).parent.parent / \"src\"\n return sorted(\n p for p in src_root.rglob(\"*.py\")\n if \"test\" not in p.parts and \"mock\" not in p.parts and p.name != \"__init__.py\"\n )\n # #endregion _collect_src_files\n\n # #region test_except_pass_replaced_with_log [C:2] [TYPE Function]\n # @BRIEF AST audit: bare `except: pass` nodes are absent from src/ source files.\n def test_except_pass_replaced_with_log(self):\n files_with_bare_except_pass = []\n\n for filepath in self._collect_src_files():\n try:\n tree = ast.parse(filepath.read_text(encoding=\"utf-8\"), filename=str(filepath))\n except SyntaxError:\n continue\n\n for node in ast.walk(tree):\n if not isinstance(node, ast.ExceptHandler):\n continue\n if node.type is not None:\n continue\n if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):\n files_with_bare_except_pass.append(str(filepath))\n break\n\n assert not files_with_bare_except_pass, (\n f\"Bare `except: pass` found in {len(files_with_bare_except_pass)} file(s):\\n\"\n + \"\\n\".join(files_with_bare_except_pass)\n )\n # #endregion test_except_pass_replaced_with_log\n\n # #region test_plugin_loader_uses_logger_not_print [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py (src/core/) uses logger for error/warning, not print().\n def test_plugin_loader_uses_logger_not_print(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n # Find all print() calls in executable code\n print_lines = []\n for node in ast.walk(tree):\n if (isinstance(node, ast.Call)\n and isinstance(node.func, ast.Name)\n and node.func.id == \"print\"):\n print_lines.append(node.lineno)\n\n assert len(print_lines) == 0, (\n f\"plugin_loader.py still contains print() calls at lines: {print_lines}\"\n )\n # #endregion test_plugin_loader_uses_logger_not_print\n\n # #region test_plugin_loader_imports_logger [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py imports a logger or _logger for structured reporting.\n def test_plugin_loader_imports_logger(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n has_logger_import = False\n for node in ast.walk(tree):\n if isinstance(node, (ast.Import, ast.ImportFrom)):\n for alias in node.names:\n if \"logger\" in alias.name.lower():\n has_logger_import = True\n break\n\n assert has_logger_import, (\n \"plugin_loader.py should import 'logger' or '_logger' \"\n \"for log-based error handling (instead of print)\"\n )\n # #endregion test_plugin_loader_imports_logger\n\n # #region test_plugin_loader_logger_calls_exist [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py references logger.error/warning/info in its code.\n def test_plugin_loader_logger_calls_exist(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n logger_calls = []\n for node in ast.walk(tree):\n if (isinstance(node, ast.Call)\n and isinstance(node.func, ast.Attribute)\n and isinstance(node.func.value, ast.Name)\n and \"logger\" in node.func.value.id.lower()):\n logger_calls.append((node.lineno, node.func.attr))\n\n error_levels = {\"error\", \"warning\", \"info\", \"debug\", \"reason\", \"reflect\", \"explore\"}\n matching = [call for call in logger_calls if call[1] in error_levels]\n assert len(matching) >= 4, (\n f\"plugin_loader.py has only {len(matching)} logger.error/warning/info calls \"\n f\"(expected at least 4 for the 4 replaced print() statements): {matching}\"\n )\n # #endregion test_plugin_loader_logger_calls_exist\n# #endregion TestLoggingAuditFixes\n" + "body": "# #region TestLoggingAuditFixes [C:3] [TYPE Module] [SEMANTICS test,logging,except,audit]\n# @BRIEF Verify Class 2 fix — no bare `except: pass` patterns remain in src/.\n# @RELATION BINDS_TO -> [EXT:frontend:PluginLoaderCore]\n# @TEST_EDGE: except_pass_replaced -> bare `except: pass` patterns are absent from src/\n# @TEST_EDGE: plugin_loader_uses_logger -> plugin_loader.py uses logger, not print\nimport ast\nfrom pathlib import Path\nimport sys\n\nsys.path.insert(0, str(Path(__file__).parent.parent / \"src\"))\n\n\nclass TestLoggingAuditFixes:\n \"\"\"Verify that all bare `except: pass` patterns have been replaced with proper logging.\"\"\"\n\n PLUGIN_LOADER_PATH = Path(__file__).parent.parent / \"src\" / \"core\" / \"plugin_loader.py\"\n\n # #region _collect_src_files [C:1] [TYPE Function]\n # @BRIEF Collect all .py files under src/ excluding test/ and mock/ directories.\n @staticmethod\n def _collect_src_files():\n src_root = Path(__file__).parent.parent / \"src\"\n return sorted(\n p for p in src_root.rglob(\"*.py\")\n if \"test\" not in p.parts and \"mock\" not in p.parts and p.name != \"__init__.py\"\n )\n # #endregion _collect_src_files\n\n # #region test_except_pass_replaced_with_log [C:2] [TYPE Function]\n # @BRIEF AST audit: bare `except: pass` nodes are absent from src/ source files.\n def test_except_pass_replaced_with_log(self):\n files_with_bare_except_pass = []\n\n for filepath in self._collect_src_files():\n try:\n tree = ast.parse(filepath.read_text(encoding=\"utf-8\"), filename=str(filepath))\n except SyntaxError:\n continue\n\n for node in ast.walk(tree):\n if not isinstance(node, ast.ExceptHandler):\n continue\n if node.type is not None:\n continue\n if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):\n files_with_bare_except_pass.append(str(filepath))\n break\n\n assert not files_with_bare_except_pass, (\n f\"Bare `except: pass` found in {len(files_with_bare_except_pass)} file(s):\\n\"\n + \"\\n\".join(files_with_bare_except_pass)\n )\n # #endregion test_except_pass_replaced_with_log\n\n # #region test_plugin_loader_uses_logger_not_print [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py (src/core/) uses logger for error/warning, not print().\n def test_plugin_loader_uses_logger_not_print(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n # Find all print() calls in executable code\n print_lines = []\n for node in ast.walk(tree):\n if (isinstance(node, ast.Call)\n and isinstance(node.func, ast.Name)\n and node.func.id == \"print\"):\n print_lines.append(node.lineno)\n\n assert len(print_lines) == 0, (\n f\"plugin_loader.py still contains print() calls at lines: {print_lines}\"\n )\n # #endregion test_plugin_loader_uses_logger_not_print\n\n # #region test_plugin_loader_imports_logger [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py imports a logger or _logger for structured reporting.\n def test_plugin_loader_imports_logger(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n has_logger_import = False\n for node in ast.walk(tree):\n if isinstance(node, (ast.Import, ast.ImportFrom)):\n for alias in node.names:\n if \"logger\" in alias.name.lower():\n has_logger_import = True\n break\n\n assert has_logger_import, (\n \"plugin_loader.py should import 'logger' or '_logger' \"\n \"for log-based error handling (instead of print)\"\n )\n # #endregion test_plugin_loader_imports_logger\n\n # #region test_plugin_loader_logger_calls_exist [C:2] [TYPE Function]\n # @BRIEF plugin_loader.py references logger.error/warning/info in its code.\n def test_plugin_loader_logger_calls_exist(self):\n code = self.PLUGIN_LOADER_PATH.read_text(encoding=\"utf-8\")\n tree = ast.parse(code, filename=str(self.PLUGIN_LOADER_PATH))\n\n logger_calls = []\n for node in ast.walk(tree):\n if (isinstance(node, ast.Call)\n and isinstance(node.func, ast.Attribute)\n and isinstance(node.func.value, ast.Name)\n and \"logger\" in node.func.value.id.lower()):\n logger_calls.append((node.lineno, node.func.attr))\n\n error_levels = {\"error\", \"warning\", \"info\", \"debug\", \"reason\", \"reflect\", \"explore\"}\n matching = [call for call in logger_calls if call[1] in error_levels]\n assert len(matching) >= 4, (\n f\"plugin_loader.py has only {len(matching)} logger.error/warning/info calls \"\n f\"(expected at least 4 for the 4 replaced print() statements): {matching}\"\n )\n # #endregion test_plugin_loader_logger_calls_exist\n# #endregion TestLoggingAuditFixes\n" }, { "contract_id": "_collect_src_files", @@ -54311,7 +55277,7 @@ "contract_type": "TestModule", "file_path": "backend/tests/test_maintenance_api.py", "start_line": 1, - "end_line": 474, + "end_line": 472, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -54322,14 +55288,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_maintenance_api [C:3] [TYPE TestModule] [SEMANTICS test, maintenance, api, fastapi, pytest]\n# @BRIEF Contract tests for Maintenance Banner API endpoints — T016, T025.\n# Tests POST /start, POST /{id}/end, POST /end-all, GET /events, GET /dashboard-banners,\n# GET /settings, PUT /settings.\n# @LAYER Test\n# @RELATION BINDS_TO -> [MaintenanceRoutesModule]\n# @TEST_CONTRACT: POST /api/maintenance/start -> 202 {task_id, maintenance_id, status}\n# @TEST_CONTRACT: POST /api/maintenance/{id}/end -> 202 {task_id, status}\n# @TEST_CONTRACT: POST /api/maintenance/end-all -> 202 {task_id, status}\n# @TEST_EDGE: missing_tables -> 422 validation error\n# @TEST_EDGE: end_time_before_start_time -> 400 validation error\n# @TEST_EDGE: non_existent_event_end -> 404 not found\nimport json\nimport os\nimport uuid\nfrom datetime import datetime, timezone, timedelta\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch, PropertyMock\n\nimport pytest\nfrom fastapi import status\n\n# Patch GitService at module level to prevent /app/storage/repositories error\n# This patches the base module BEFORE any imports can reach it\nimport sys\nfrom unittest.mock import MagicMock\n\n_mock_git_svc_cls = MagicMock()\n_mock_git_svc_cls.return_value = MagicMock()\nsys.modules['src.services.git._base'] = MagicMock(GitService=_mock_git_svc_cls)\nsys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient with all dependencies mocked.\"\"\"\n from fastapi.testclient import TestClient\n from src.app import app\n return TestClient(app)\n\n\n@pytest.fixture(autouse=True)\ndef auth_mock(client, mock_db):\n \"\"\"Create a valid API key with maintenance permissions and mock JWT user.\"\"\"\n from src.core.auth.api_key import generate_api_key\n from src.models.api_key import APIKey\n from src.dependencies import get_current_user\n from src.app import app\n\n # Create API key for mutation endpoints\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test API Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\", \"maintenance:end_all\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n client.headers.update({\"X-API-Key\": raw})\n\n # Mock JWT user for read-only endpoints\n mock_role = MagicMock()\n mock_role.name = \"Admin\"\n mock_role.permissions = []\n\n mock_user = MagicMock()\n mock_user.username = \"test-admin\"\n mock_user.roles = [mock_role]\n\n async def _mock_get_current_user():\n return mock_user\n\n app.dependency_overrides[get_current_user] = _mock_get_current_user\n\n yield\n\n client.headers.pop(\"X-API-Key\", None)\n app.dependency_overrides = {}\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency to return a clean in-memory session.\n \n Uses StaticPool so all Session() instances share the same connection,\n preventing SQLite threading issues where each :memory: connection\n creates a separate database.\n Creates all tables on the shared Base.metadata.\n \"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker, Session as SASession\n from sqlalchemy.pool import StaticPool\n from src.models.mapping import Base\n from src.dependencies import get_db\n from src.models.maintenance import MaintenanceSettings, DashboardScope\n\n # StaticPool ensures all sessions share the same in-memory connection\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n echo=False,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Create default settings\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=\"test-env\",\n display_timezone=\"UTC\",\n banner_template=\"Test: {message} ({start_time}-{end_time})\",\n dashboard_scope=DashboardScope.PUBLISHED_ONLY,\n excluded_dashboard_ids=[],\n forced_dashboard_ids=[],\n )\n session.add(settings)\n session.commit()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n from src.app import app\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n@pytest.fixture\ndef mock_task_manager():\n \"\"\"Patch get_task_manager to return a mock.\"\"\"\n from src.dependencies import get_task_manager\n\n mock_tm = MagicMock()\n mock_task = MagicMock()\n mock_task.id = \"test-task-id-123\"\n mock_tm.create_task = AsyncMock(return_value=mock_task)\n\n from src.app import app\n app.dependency_overrides[get_task_manager] = lambda: mock_tm\n yield mock_tm\n app.dependency_overrides.pop(get_task_manager, None)\n\n\n# ── T016: POST /api/maintenance/start ─────────────────────────\n\nclass TestStartEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/start.\"\"\"\n\n # #region test_start_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid request returns 202 with task_id and maintenance_id.\n def test_start_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"message\": \"Test maintenance\",\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert \"maintenance_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_start_returns_202\n\n # #region test_start_missing_tables [C:2] [TYPE Function]\n # @BRIEF Missing tables field returns 422.\n def test_start_missing_tables(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n },\n )\n assert response.status_code == 422\n # #endregion test_start_missing_tables\n\n # #region test_start_end_time_before_start [C:2] [TYPE Function]\n # @BRIEF end_time before start_time returns 400.\n def test_start_end_time_before_start(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 400\n # #endregion test_start_end_time_before_start\n\n # #region test_start_too_many_tables [C:2] [TYPE Function]\n # @BRIEF More than 100 tables returns 422 (Pydantic validation).\n def test_start_too_many_tables(self, client, mock_db, mock_task_manager):\n tables = [f\"raw.table{i}\" for i in range(101)]\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": tables,\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 422\n # #endregion test_start_too_many_tables\n\n # #region test_start_idempotency [C:2] [TYPE Function]\n # @BRIEF Same (tables, start, end) returns already_active.\n def test_start_idempotency(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n payload = {\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"environment_id\": \"test-env\",\n }\n\n # First call creates event\n response1 = client.post(\"/api/maintenance/start\", json=payload)\n assert response1.status_code == 202\n\n # Manually set event to ACTIVE so idempotency check finds it\n maintenance_id = response1.json()[\"maintenance_id\"]\n event = mock_db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n if event:\n event.status = MaintenanceEventStatus.ACTIVE\n mock_db.commit()\n\n # Second call with same params — idempotent\n response2 = client.post(\"/api/maintenance/start\", json=payload)\n assert response2.status_code == 202\n data = response2.json()\n # Should have maintenance_id and status\n assert data.get(\"maintenance_id\") or data.get(\"status\")\n # #endregion test_start_idempotency\n\n\n# ── T025: POST /api/maintenance/{id}/end ──────────────────────\n\nclass TestEndEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/{id}/end.\"\"\"\n\n # #region test_end_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid maintenance_id returns 202.\n def test_end_returns_202(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.post(f\"/api/maintenance/{event.id}/end\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n # #endregion test_end_returns_202\n\n # #region test_end_not_found [C:2] [TYPE Function]\n # @BRIEF Non-existent maintenance_id returns 404.\n def test_end_not_found(self, client, mock_db, mock_task_manager):\n response = client.post(\"/api/maintenance/nonexistent-id/end\")\n assert response.status_code == 404\n # #endregion test_end_not_found\n\n\n# ── POST /api/maintenance/end-all ─────────────────────────────\n\nclass TestEndAllEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/end-all.\"\"\"\n\n # #region test_end_all_returns_202 [C:2] [TYPE Function]\n # @BRIEF end-all returns 202.\n def test_end_all_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\"/api/maintenance/end-all\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_end_all_returns_202\n\n\n# ── GET /api/maintenance/events ───────────────────────────────\n\nclass TestEventsEndpoint:\n \"\"\"Contract tests for GET /api/maintenance/events.\"\"\"\n\n # #region test_list_events [C:2] [TYPE Function]\n # @BRIEF Returns active and completed events.\n def test_list_events(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n active = MaintenanceEvent(\n tables=[\"raw.active\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(active)\n\n completed = MaintenanceEvent(\n tables=[\"raw.done\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.COMPLETED,\n environment_id=\"test-env\",\n )\n mock_db.add(completed)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n data = response.json()\n assert \"active\" in data\n assert \"completed\" in data\n # #endregion test_list_events\n\n # #region test_expired_event_auto_ends [C:2] [TYPE Function]\n # @BRIEF Event past end_time with ACTIVE status → create_task called with auto-end params.\n def test_expired_event_auto_ends(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) - timedelta(hours=2),\n end_time=datetime.now(timezone.utc) - timedelta(hours=1),\n message=\"Expired maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_called_once_with(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": \"test-env\",\n \"user\": \"system\",\n },\n )\n # #endregion test_expired_event_auto_ends\n\n # #region test_future_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with future end_time → no task created, skipped by < now filter.\n def test_future_end_time_skipped(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) + timedelta(hours=1),\n end_time=datetime.now(timezone.utc) + timedelta(hours=3),\n message=\"Future maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_future_end_time_skipped\n\n # #region test_no_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with end_time=None → no task created, skipped by isnot(None) filter.\n def test_no_end_time_skipped(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) - timedelta(hours=2),\n end_time=None,\n message=\"No end time\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_no_end_time_skipped\n\n\n# ── GET /api/maintenance/dashboard-banners ────────────────────\n\nclass TestDashboardBannersEndpoint:\n \"\"\"Contract tests for GET /api/maintenance/dashboard-banners.\"\"\"\n\n # #region test_list_banners [C:2] [TYPE Function]\n # @BRIEF Returns list of active banners.\n def test_list_banners(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n )\n\n banner = MaintenanceDashboardBanner(\n environment_id=\"test-env\",\n dashboard_id=101,\n chart_id=12345,\n banner_text=\"Test\",\n status=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n mock_db.add(banner)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/dashboard-banners\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n # #endregion test_list_banners\n\n\n# ── GET /api/maintenance/settings ─────────────────────────────\n\nclass TestSettingsEndpoint:\n \"\"\"Contract tests for GET/PUT /api/maintenance/settings.\"\"\"\n\n # #region test_get_settings [C:2] [TYPE Function]\n # @BRIEF GET returns settings.\n def test_get_settings(self, client, mock_db, mock_task_manager):\n response = client.get(\"/api/maintenance/settings\")\n assert response.status_code == 200\n data = response.json()\n assert \"target_environment_id\" in data\n assert data[\"target_environment_id\"] == \"test-env\"\n # #endregion test_get_settings\n\n # #region test_put_settings [C:2] [TYPE Function]\n # @BRIEF PUT updates settings.\n def test_put_settings(self, client, mock_db, mock_task_manager):\n response = client.put(\n \"/api/maintenance/settings\",\n json={\"display_timezone\": \"Europe/Moscow\"},\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"display_timezone\"] == \"Europe/Moscow\"\n # #endregion test_put_settings\n# #endregion test_maintenance_api\n" + "body": "# #region test_maintenance_api [C:3] [TYPE TestModule] [SEMANTICS test, maintenance, api, fastapi, pytest]\n# @BRIEF Contract tests for Maintenance Banner API endpoints — T016, T025.\n# Tests POST /start, POST /{id}/end, POST /end-all, GET /events, GET /dashboard-banners,\n# GET /settings, PUT /settings.\n# @LAYER Test\n# @RELATION BINDS_TO -> [MaintenanceRoutesModule]\n# @TEST_CONTRACT: POST /api/maintenance/start -> 202 {task_id, maintenance_id, status}\n# @TEST_CONTRACT: POST /api/maintenance/{id}/end -> 202 {task_id, status}\n# @TEST_CONTRACT: POST /api/maintenance/end-all -> 202 {task_id, status}\n# @TEST_EDGE: missing_tables -> 422 validation error\n# @TEST_EDGE: end_time_before_start_time -> 400 validation error\n# @TEST_EDGE: non_existent_event_end -> 404 not found\nfrom datetime import UTC, datetime, timedelta\nimport pytest\n\n# Patch GitService at module level to prevent /app/storage/repositories error\n# This patches the base module BEFORE any imports can reach it\nimport sys\nfrom unittest.mock import AsyncMock, MagicMock\n\n_mock_git_svc_cls = MagicMock()\n_mock_git_svc_cls.return_value = MagicMock()\nsys.modules['src.services.git._base'] = MagicMock(GitService=_mock_git_svc_cls)\nsys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n@pytest.fixture\ndef client():\n \"\"\"Create a TestClient with all dependencies mocked.\"\"\"\n from fastapi.testclient import TestClient\n\n from src.app import app\n return TestClient(app)\n\n\n@pytest.fixture(autouse=True)\ndef auth_mock(client, mock_db):\n \"\"\"Create a valid API key with maintenance permissions and mock JWT user.\"\"\"\n from src.app import app\n from src.core.auth.api_key import generate_api_key\n from src.dependencies import get_current_user\n from src.models.api_key import APIKey\n\n # Create API key for mutation endpoints\n raw, prefix, key_hash = generate_api_key()\n api_key = APIKey(\n key_hash=key_hash,\n prefix=prefix,\n name=\"Test API Key\",\n permissions=[\"maintenance:start\", \"maintenance:end\", \"maintenance:end_all\"],\n active=True,\n )\n mock_db.add(api_key)\n mock_db.commit()\n\n client.headers.update({\"X-API-Key\": raw})\n\n # Mock JWT user for read-only endpoints\n mock_role = MagicMock()\n mock_role.name = \"Admin\"\n mock_role.permissions = []\n\n mock_user = MagicMock()\n mock_user.username = \"test-admin\"\n mock_user.roles = [mock_role]\n\n async def _mock_get_current_user():\n return mock_user\n\n app.dependency_overrides[get_current_user] = _mock_get_current_user\n\n yield\n\n client.headers.pop(\"X-API-Key\", None)\n app.dependency_overrides = {}\n\n\n@pytest.fixture\ndef mock_db():\n \"\"\"Patch get_db dependency to return a clean in-memory session.\n \n Uses StaticPool so all Session() instances share the same connection,\n preventing SQLite threading issues where each :memory: connection\n creates a separate database.\n Creates all tables on the shared Base.metadata.\n \"\"\"\n from sqlalchemy import create_engine\n from sqlalchemy.orm import sessionmaker\n from sqlalchemy.pool import StaticPool\n\n from src.dependencies import get_db\n from src.models.maintenance import DashboardScope, MaintenanceSettings\n from src.models.mapping import Base\n\n # StaticPool ensures all sessions share the same in-memory connection\n engine = create_engine(\n \"sqlite:///:memory:\",\n poolclass=StaticPool,\n connect_args={\"check_same_thread\": False},\n echo=False,\n )\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Create default settings\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=\"test-env\",\n display_timezone=\"UTC\",\n banner_template=\"Test: {message} ({start_time}-{end_time})\",\n dashboard_scope=DashboardScope.PUBLISHED_ONLY,\n excluded_dashboard_ids=[],\n forced_dashboard_ids=[],\n )\n session.add(settings)\n session.commit()\n\n def _get_db_override():\n db = Session()\n try:\n yield db\n finally:\n db.close()\n\n from src.app import app\n app.dependency_overrides[get_db] = _get_db_override\n yield session\n app.dependency_overrides.pop(get_db, None)\n session.close()\n\n\n@pytest.fixture\ndef mock_task_manager():\n \"\"\"Patch get_task_manager to return a mock.\"\"\"\n from src.dependencies import get_task_manager\n\n mock_tm = MagicMock()\n mock_task = MagicMock()\n mock_task.id = \"test-task-id-123\"\n mock_tm.create_task = AsyncMock(return_value=mock_task)\n\n from src.app import app\n app.dependency_overrides[get_task_manager] = lambda: mock_tm\n yield mock_tm\n app.dependency_overrides.pop(get_task_manager, None)\n\n\n# ── T016: POST /api/maintenance/start ─────────────────────────\n\nclass TestStartEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/start.\"\"\"\n\n # #region test_start_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid request returns 202 with task_id and maintenance_id.\n def test_start_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"message\": \"Test maintenance\",\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert \"maintenance_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_start_returns_202\n\n # #region test_start_missing_tables [C:2] [TYPE Function]\n # @BRIEF Missing tables field returns 422.\n def test_start_missing_tables(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n },\n )\n assert response.status_code == 422\n # #endregion test_start_missing_tables\n\n # #region test_start_end_time_before_start [C:2] [TYPE Function]\n # @BRIEF end_time before start_time returns 400.\n def test_start_end_time_before_start(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 400\n # #endregion test_start_end_time_before_start\n\n # #region test_start_too_many_tables [C:2] [TYPE Function]\n # @BRIEF More than 100 tables returns 422 (Pydantic validation).\n def test_start_too_many_tables(self, client, mock_db, mock_task_manager):\n tables = [f\"raw.table{i}\" for i in range(101)]\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": tables,\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 422\n # #endregion test_start_too_many_tables\n\n # #region test_start_idempotency [C:2] [TYPE Function]\n # @BRIEF Same (tables, start, end) returns already_active.\n def test_start_idempotency(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n payload = {\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"environment_id\": \"test-env\",\n }\n\n # First call creates event\n response1 = client.post(\"/api/maintenance/start\", json=payload)\n assert response1.status_code == 202\n\n # Manually set event to ACTIVE so idempotency check finds it\n maintenance_id = response1.json()[\"maintenance_id\"]\n event = mock_db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n if event:\n event.status = MaintenanceEventStatus.ACTIVE\n mock_db.commit()\n\n # Second call with same params — idempotent\n response2 = client.post(\"/api/maintenance/start\", json=payload)\n assert response2.status_code == 202\n data = response2.json()\n # Should have maintenance_id and status\n assert data.get(\"maintenance_id\") or data.get(\"status\")\n # #endregion test_start_idempotency\n\n\n# ── T025: POST /api/maintenance/{id}/end ──────────────────────\n\nclass TestEndEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/{id}/end.\"\"\"\n\n # #region test_end_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid maintenance_id returns 202.\n def test_end_returns_202(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.post(f\"/api/maintenance/{event.id}/end\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n # #endregion test_end_returns_202\n\n # #region test_end_not_found [C:2] [TYPE Function]\n # @BRIEF Non-existent maintenance_id returns 404.\n def test_end_not_found(self, client, mock_db, mock_task_manager):\n response = client.post(\"/api/maintenance/nonexistent-id/end\")\n assert response.status_code == 404\n # #endregion test_end_not_found\n\n\n# ── POST /api/maintenance/end-all ─────────────────────────────\n\nclass TestEndAllEndpoint:\n \"\"\"Contract tests for POST /api/maintenance/end-all.\"\"\"\n\n # #region test_end_all_returns_202 [C:2] [TYPE Function]\n # @BRIEF end-all returns 202.\n def test_end_all_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\"/api/maintenance/end-all\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_end_all_returns_202\n\n\n# ── GET /api/maintenance/events ───────────────────────────────\n\nclass TestEventsEndpoint:\n \"\"\"Contract tests for GET /api/maintenance/events.\"\"\"\n\n # #region test_list_events [C:2] [TYPE Function]\n # @BRIEF Returns active and completed events.\n def test_list_events(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n active = MaintenanceEvent(\n tables=[\"raw.active\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(active)\n\n completed = MaintenanceEvent(\n tables=[\"raw.done\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.COMPLETED,\n environment_id=\"test-env\",\n )\n mock_db.add(completed)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n data = response.json()\n assert \"active\" in data\n assert \"completed\" in data\n # #endregion test_list_events\n\n # #region test_expired_event_auto_ends [C:2] [TYPE Function]\n # @BRIEF Event past end_time with ACTIVE status → create_task called with auto-end params.\n def test_expired_event_auto_ends(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) - timedelta(hours=2),\n end_time=datetime.now(UTC) - timedelta(hours=1),\n message=\"Expired maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_called_once_with(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": \"test-env\",\n \"user\": \"system\",\n },\n )\n # #endregion test_expired_event_auto_ends\n\n # #region test_future_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with future end_time → no task created, skipped by < now filter.\n def test_future_end_time_skipped(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) + timedelta(hours=1),\n end_time=datetime.now(UTC) + timedelta(hours=3),\n message=\"Future maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_future_end_time_skipped\n\n # #region test_no_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with end_time=None → no task created, skipped by isnot(None) filter.\n def test_no_end_time_skipped(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) - timedelta(hours=2),\n end_time=None,\n message=\"No end time\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_no_end_time_skipped\n\n\n# ── GET /api/maintenance/dashboard-banners ────────────────────\n\nclass TestDashboardBannersEndpoint:\n \"\"\"Contract tests for GET /api/maintenance/dashboard-banners.\"\"\"\n\n # #region test_list_banners [C:2] [TYPE Function]\n # @BRIEF Returns list of active banners.\n def test_list_banners(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import (\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n )\n\n banner = MaintenanceDashboardBanner(\n environment_id=\"test-env\",\n dashboard_id=101,\n chart_id=12345,\n banner_text=\"Test\",\n status=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n mock_db.add(banner)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/dashboard-banners\")\n assert response.status_code == 200\n data = response.json()\n assert isinstance(data, list)\n # #endregion test_list_banners\n\n\n# ── GET /api/maintenance/settings ─────────────────────────────\n\nclass TestSettingsEndpoint:\n \"\"\"Contract tests for GET/PUT /api/maintenance/settings.\"\"\"\n\n # #region test_get_settings [C:2] [TYPE Function]\n # @BRIEF GET returns settings.\n def test_get_settings(self, client, mock_db, mock_task_manager):\n response = client.get(\"/api/maintenance/settings\")\n assert response.status_code == 200\n data = response.json()\n assert \"target_environment_id\" in data\n assert data[\"target_environment_id\"] == \"test-env\"\n # #endregion test_get_settings\n\n # #region test_put_settings [C:2] [TYPE Function]\n # @BRIEF PUT updates settings.\n def test_put_settings(self, client, mock_db, mock_task_manager):\n response = client.put(\n \"/api/maintenance/settings\",\n json={\"display_timezone\": \"Europe/Moscow\"},\n )\n assert response.status_code == 200\n data = response.json()\n assert data[\"display_timezone\"] == \"Europe/Moscow\"\n # #endregion test_put_settings\n# #endregion test_maintenance_api\n" }, { "contract_id": "test_start_returns_202", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 161, - "end_line": 179, + "start_line": 156, + "end_line": 174, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54340,14 +55306,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_start_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid request returns 202 with task_id and maintenance_id.\n def test_start_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"message\": \"Test maintenance\",\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert \"maintenance_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_start_returns_202\n" + "body": " # #region test_start_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid request returns 202 with task_id and maintenance_id.\n def test_start_returns_202(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"message\": \"Test maintenance\",\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n assert \"maintenance_id\" in data\n assert data[\"status\"] == \"pending\"\n # #endregion test_start_returns_202\n" }, { "contract_id": "test_start_missing_tables", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 181, - "end_line": 191, + "start_line": 176, + "end_line": 186, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54358,14 +55324,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_start_missing_tables [C:2] [TYPE Function]\n # @BRIEF Missing tables field returns 422.\n def test_start_missing_tables(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n },\n )\n assert response.status_code == 422\n # #endregion test_start_missing_tables\n" + "body": " # #region test_start_missing_tables [C:2] [TYPE Function]\n # @BRIEF Missing tables field returns 422.\n def test_start_missing_tables(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n },\n )\n assert response.status_code == 422\n # #endregion test_start_missing_tables\n" }, { "contract_id": "test_start_end_time_before_start", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 193, - "end_line": 206, + "start_line": 188, + "end_line": 201, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54376,14 +55342,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_start_end_time_before_start [C:2] [TYPE Function]\n # @BRIEF end_time before start_time returns 400.\n def test_start_end_time_before_start(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 400\n # #endregion test_start_end_time_before_start\n" + "body": " # #region test_start_end_time_before_start [C:2] [TYPE Function]\n # @BRIEF end_time before start_time returns 400.\n def test_start_end_time_before_start(self, client, mock_db, mock_task_manager):\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 400\n # #endregion test_start_end_time_before_start\n" }, { "contract_id": "test_start_too_many_tables", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 208, - "end_line": 221, + "start_line": 203, + "end_line": 216, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54394,14 +55360,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_start_too_many_tables [C:2] [TYPE Function]\n # @BRIEF More than 100 tables returns 422 (Pydantic validation).\n def test_start_too_many_tables(self, client, mock_db, mock_task_manager):\n tables = [f\"raw.table{i}\" for i in range(101)]\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": tables,\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 422\n # #endregion test_start_too_many_tables\n" + "body": " # #region test_start_too_many_tables [C:2] [TYPE Function]\n # @BRIEF More than 100 tables returns 422 (Pydantic validation).\n def test_start_too_many_tables(self, client, mock_db, mock_task_manager):\n tables = [f\"raw.table{i}\" for i in range(101)]\n response = client.post(\n \"/api/maintenance/start\",\n json={\n \"tables\": tables,\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"environment_id\": \"test-env\",\n },\n )\n assert response.status_code == 422\n # #endregion test_start_too_many_tables\n" }, { "contract_id": "test_start_idempotency", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 223, - "end_line": 254, + "start_line": 218, + "end_line": 249, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54412,14 +55378,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_start_idempotency [C:2] [TYPE Function]\n # @BRIEF Same (tables, start, end) returns already_active.\n def test_start_idempotency(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n payload = {\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),\n \"environment_id\": \"test-env\",\n }\n\n # First call creates event\n response1 = client.post(\"/api/maintenance/start\", json=payload)\n assert response1.status_code == 202\n\n # Manually set event to ACTIVE so idempotency check finds it\n maintenance_id = response1.json()[\"maintenance_id\"]\n event = mock_db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n if event:\n event.status = MaintenanceEventStatus.ACTIVE\n mock_db.commit()\n\n # Second call with same params — idempotent\n response2 = client.post(\"/api/maintenance/start\", json=payload)\n assert response2.status_code == 202\n data = response2.json()\n # Should have maintenance_id and status\n assert data.get(\"maintenance_id\") or data.get(\"status\")\n # #endregion test_start_idempotency\n" + "body": " # #region test_start_idempotency [C:2] [TYPE Function]\n # @BRIEF Same (tables, start, end) returns already_active.\n def test_start_idempotency(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n payload = {\n \"tables\": [\"raw.sales\"],\n \"start_time\": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),\n \"end_time\": (datetime.now(UTC) + timedelta(hours=3)).isoformat(),\n \"environment_id\": \"test-env\",\n }\n\n # First call creates event\n response1 = client.post(\"/api/maintenance/start\", json=payload)\n assert response1.status_code == 202\n\n # Manually set event to ACTIVE so idempotency check finds it\n maintenance_id = response1.json()[\"maintenance_id\"]\n event = mock_db.query(MaintenanceEvent).filter(\n MaintenanceEvent.id == maintenance_id\n ).first()\n if event:\n event.status = MaintenanceEventStatus.ACTIVE\n mock_db.commit()\n\n # Second call with same params — idempotent\n response2 = client.post(\"/api/maintenance/start\", json=payload)\n assert response2.status_code == 202\n data = response2.json()\n # Should have maintenance_id and status\n assert data.get(\"maintenance_id\") or data.get(\"status\")\n # #endregion test_start_idempotency\n" }, { "contract_id": "test_end_returns_202", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 262, - "end_line": 280, + "start_line": 257, + "end_line": 275, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54430,14 +55396,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_end_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid maintenance_id returns 202.\n def test_end_returns_202(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.post(f\"/api/maintenance/{event.id}/end\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n # #endregion test_end_returns_202\n" + "body": " # #region test_end_returns_202 [C:2] [TYPE Function]\n # @BRIEF Valid maintenance_id returns 202.\n def test_end_returns_202(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.post(f\"/api/maintenance/{event.id}/end\")\n assert response.status_code == 202\n data = response.json()\n assert \"task_id\" in data\n # #endregion test_end_returns_202\n" }, { "contract_id": "test_end_not_found", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 282, - "end_line": 287, + "start_line": 277, + "end_line": 282, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54454,8 +55420,8 @@ "contract_id": "test_end_all_returns_202", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 295, - "end_line": 303, + "start_line": 290, + "end_line": 298, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54472,8 +55438,8 @@ "contract_id": "test_list_events", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 311, - "end_line": 338, + "start_line": 306, + "end_line": 333, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54484,14 +55450,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_list_events [C:2] [TYPE Function]\n # @BRIEF Returns active and completed events.\n def test_list_events(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n active = MaintenanceEvent(\n tables=[\"raw.active\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(active)\n\n completed = MaintenanceEvent(\n tables=[\"raw.done\"],\n start_time=datetime.now(timezone.utc),\n status=MaintenanceEventStatus.COMPLETED,\n environment_id=\"test-env\",\n )\n mock_db.add(completed)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n data = response.json()\n assert \"active\" in data\n assert \"completed\" in data\n # #endregion test_list_events\n" + "body": " # #region test_list_events [C:2] [TYPE Function]\n # @BRIEF Returns active and completed events.\n def test_list_events(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n active = MaintenanceEvent(\n tables=[\"raw.active\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(active)\n\n completed = MaintenanceEvent(\n tables=[\"raw.done\"],\n start_time=datetime.now(UTC),\n status=MaintenanceEventStatus.COMPLETED,\n environment_id=\"test-env\",\n )\n mock_db.add(completed)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n data = response.json()\n assert \"active\" in data\n assert \"completed\" in data\n # #endregion test_list_events\n" }, { "contract_id": "test_expired_event_auto_ends", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 340, - "end_line": 369, + "start_line": 335, + "end_line": 365, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54502,14 +55468,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_expired_event_auto_ends [C:2] [TYPE Function]\n # @BRIEF Event past end_time with ACTIVE status → create_task called with auto-end params.\n def test_expired_event_auto_ends(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) - timedelta(hours=2),\n end_time=datetime.now(timezone.utc) - timedelta(hours=1),\n message=\"Expired maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_called_once_with(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": \"test-env\",\n \"user\": \"system\",\n },\n )\n # #endregion test_expired_event_auto_ends\n" + "body": " # #region test_expired_event_auto_ends [C:2] [TYPE Function]\n # @BRIEF Event past end_time with ACTIVE status → create_task called with auto-end params.\n def test_expired_event_auto_ends(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) - timedelta(hours=2),\n end_time=datetime.now(UTC) - timedelta(hours=1),\n message=\"Expired maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_called_once_with(\n plugin_id=\"maintenance_banner_apply\",\n params={\n \"operation\": \"end\",\n \"event_id\": event.id,\n \"environment_id\": \"test-env\",\n \"user\": \"system\",\n },\n )\n # #endregion test_expired_event_auto_ends\n" }, { "contract_id": "test_future_end_time_skipped", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 371, - "end_line": 392, + "start_line": 367, + "end_line": 389, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54520,14 +55486,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_future_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with future end_time → no task created, skipped by < now filter.\n def test_future_end_time_skipped(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) + timedelta(hours=1),\n end_time=datetime.now(timezone.utc) + timedelta(hours=3),\n message=\"Future maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_future_end_time_skipped\n" + "body": " # #region test_future_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with future end_time → no task created, skipped by < now filter.\n def test_future_end_time_skipped(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) + timedelta(hours=1),\n end_time=datetime.now(UTC) + timedelta(hours=3),\n message=\"Future maintenance\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_future_end_time_skipped\n" }, { "contract_id": "test_no_end_time_skipped", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 394, - "end_line": 415, + "start_line": 391, + "end_line": 413, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54538,14 +55504,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " # #region test_no_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with end_time=None → no task created, skipped by isnot(None) filter.\n def test_no_end_time_skipped(self, client, mock_db, mock_task_manager):\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n from datetime import timedelta\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(timezone.utc) - timedelta(hours=2),\n end_time=None,\n message=\"No end time\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_no_end_time_skipped\n" + "body": " # #region test_no_end_time_skipped [C:2] [TYPE Function]\n # @BRIEF Event with end_time=None → no task created, skipped by isnot(None) filter.\n def test_no_end_time_skipped(self, client, mock_db, mock_task_manager):\n from datetime import timedelta\n\n from src.models.maintenance import MaintenanceEvent, MaintenanceEventStatus\n\n event = MaintenanceEvent(\n tables=[\"raw.sales\"],\n start_time=datetime.now(UTC) - timedelta(hours=2),\n end_time=None,\n message=\"No end time\",\n status=MaintenanceEventStatus.ACTIVE,\n environment_id=\"test-env\",\n )\n mock_db.add(event)\n mock_db.commit()\n\n response = client.get(\"/api/maintenance/events\")\n assert response.status_code == 200\n\n mock_task_manager.create_task.assert_not_called()\n # #endregion test_no_end_time_skipped\n" }, { "contract_id": "test_list_banners", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 423, - "end_line": 445, + "start_line": 421, + "end_line": 443, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54562,8 +55528,8 @@ "contract_id": "test_get_settings", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 453, - "end_line": 461, + "start_line": 451, + "end_line": 459, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54580,8 +55546,8 @@ "contract_id": "test_put_settings", "contract_type": "Function", "file_path": "backend/tests/test_maintenance_api.py", - "start_line": 463, - "end_line": 473, + "start_line": 461, + "end_line": 471, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -54599,7 +55565,7 @@ "contract_type": "TestModule", "file_path": "backend/tests/test_maintenance_service.py", "start_line": 1, - "end_line": 623, + "end_line": 620, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -54610,14 +55576,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "# #region test_maintenance_service [C:3] [TYPE TestModule] [SEMANTICS test, maintenance, service, pytest, integration]\n# @BRIEF Integration tests for MaintenanceService — T018, T026, T026a.\n# Tests the C3/C4 orchestrators: start_maintenance, end_maintenance, end_all_maintenance,\n# and helpers: find_affected_dashboards, ensure_banner_chart, build_banner_text, rebuild_banner.\n# @LAYER Test\n# @RELATION BINDS_TO -> [EXT:frontend:MaintenanceServiceModule]\n# @TEST_CONTRACT: start_maintenance(str, Session, SupersetClient) -> dict\n# @TEST_CONTRACT: end_maintenance(str, Session, SupersetClient) -> dict\n# @TEST_CONTRACT: end_all_maintenance(Session, SupersetClient) -> dict\n# @TEST_EDGE: event_not_found -> returns failed status with error message\n# @TEST_EDGE: no_matching_dashboards -> returns \"no_match\" status\n# @TEST_EDGE: superset_api_failure -> returns partial/failed with error details\nimport uuid\nfrom datetime import datetime, timezone\nfrom typing import Any, cast\nfrom unittest.mock import MagicMock, patch, PropertyMock\n\nimport pytest\n\nfrom src.models.maintenance import (\n DashboardScope,\n MaintenanceDashboardBanner,\n MaintenanceDashboardBannerStatus,\n MaintenanceDashboardState,\n MaintenanceDashboardStateStatus,\n MaintenanceEvent,\n MaintenanceEventStatus,\n MaintenanceSettings,\n)\nfrom src.services.maintenance import (\n build_banner_text,\n build_idempotency_key,\n end_all_maintenance,\n end_maintenance,\n ensure_banner_chart,\n find_affected_dashboards,\n rebuild_banner,\n start_maintenance,\n)\n\n\n# ── Fixtures ──────────────────────────────────────────────────\n\n@pytest.fixture\ndef mock_superset():\n \"\"\"Create a mock SupersetClient for testing.\"\"\"\n client = MagicMock()\n # Default: no datasets\n client.get_datasets.return_value = (0, [])\n client.get_dashboards.return_value = (0, [])\n client.get_dataset_detail.return_value = {\"linked_dashboards\": []}\n client.create_markdown_chart.return_value = 12345\n client.get_chart.return_value = {\"id\": 12345, \"result\": {\"viz_type\": \"markdown\"}}\n client.update_banner_on_dashboard.return_value = {\"result\": \"ok\"}\n client.update_dashboard_layout.return_value = {\"result\": \"ok\"}\n client.remove_chart_from_layout.return_value = {\"result\": \"ok\"}\n client.delete_chart.return_value = {\"result\": \"ok\"}\n return client\n\n\n@pytest.fixture\ndef db_session():\n \"\"\"Create an in-memory SQLite session with tables.\"\"\"\n from sqlalchemy import create_engine, event\n from sqlalchemy.orm import sessionmaker\n from src.models.mapping import Base\n\n engine = create_engine(\"sqlite:///:memory:\", echo=False)\n event.listen(engine, \"connect\", lambda c, _: c.execute(\"PRAGMA foreign_keys=ON\"))\n Base.metadata.create_all(engine)\n Session = sessionmaker(bind=engine)\n session = Session()\n\n # Create default settings\n settings = MaintenanceSettings(\n id=\"default\",\n target_environment_id=\"test-env\",\n display_timezone=\"UTC\",\n banner_template=\"Maintenance: {message} ({start_time} - {end_time})\",\n dashboard_scope=DashboardScope.PUBLISHED_ONLY,\n excluded_dashboard_ids=[],\n forced_dashboard_ids=[],\n )\n session.add(settings)\n session.commit()\n return session\n\n\n@pytest.fixture\ndef pending_event(db_session):\n \"\"\"Create a PENDING maintenance event in the DB.\"\"\"\n event = MaintenanceEvent(\n tables=[\"raw.sales\", \"raw.inventory\"],\n start_time=datetime.now(timezone.utc),\n end_time=datetime.now(timezone.utc),\n message=\"Test maintenance\",\n status=MaintenanceEventStatus.PENDING,\n environment_id=\"test-env\",\n )\n db_session.add(event)\n db_session.commit()\n return event\n\n\n# ── T017: build_idempotency_key tests ─────────────────────────\n\nclass TestBuildIdempotencyKey:\n \"\"\"Tests for build_idempotency_key helper.\"\"\"\n\n # #region test_basic_key [C:2] [TYPE Function]\n # @BRIEF Basic idempotency key with tables and times.\n def test_basic_key(self):\n tables = [\"raw.sales\", \"RAW.INVENTORY\"]\n start = datetime(2026, 5, 21, 22, 0, 0, tzinfo=timezone.utc)\n end = datetime(2026, 5, 22, 2, 0, 0, tzinfo=timezone.utc)\n key = build_idempotency_key(tables, start, end)\n assert key[0] == frozenset({\"raw.inventory\", \"raw.sales\"})\n assert key[1] == \"2026-05-21T22:00:00+00:00\"\n assert key[2] == \"2026-05-22T02:00:00+00:00\"\n # #endregion test_basic_key\n\n # #region test_key_without_end_time [C:2] [TYPE Function]\n # @BRIEF Idempotency key with None end_time.\n def test_key_without_end_time(self):\n tables = [\"raw.sales\"]\n start = datetime(2026, 5, 21, 22, 0, 0, tzinfo=timezone.utc)\n key = build_idempotency_key(tables, start, None)\n assert key[0] == frozenset({\"raw.sales\"})\n assert key[1] == \"2026-05-21T22:00:00+00:00\"\n assert key[2] is None\n # #endregion test_key_without_end_time\n\n # #region test_key_table_sorting [C:2] [TYPE Function]\n # @BRIEF Tables are sorted alphabetically in frozenset.\n def test_key_table_sorting(self):\n key1 = build_idempotency_key([\"z.table\", \"a.table\"], None, None)\n key2 = build_idempotency_key([\"a.table\", \"z.table\"], None, None)\n assert key1 == key2\n # #endregion test_key_table_sorting\n\n\n# ── T019: find_affected_dashboards tests ──────────────────────\n\nclass TestFindAffectedDashboards:\n \"\"\"Tests for find_affected_dashboards.\"\"\"\n\n # #region test_empty_tables [C:2] [TYPE Function]\n # @BRIEF Empty tables list returns empty.\n def test_empty_tables(self, mock_superset):\n result = find_affected_dashboards([], mock_superset)\n assert result == []\n # #endregion test_empty_tables\n\n # #region test_table_based_match [C:2] [TYPE Function]\n # @BRIEF Table-based dataset matching.\n def test_table_based_match(self, mock_superset):\n mock_superset.get_datasets.return_value = (2, [\n {\"id\": 1, \"schema\": \"raw\", \"table_name\": \"sales\", \"sql\": None},\n {\"id\": 2, \"schema\": \"raw\", \"table_name\": \"inventory\", \"sql\": None},\n ])\n mock_superset.get_dataset_detail.side_effect = [\n {\"linked_dashboards\": [{\"id\": 101}]},\n {\"linked_dashboards\": [{\"id\": 102}]},\n ]\n\n result = find_affected_dashboards([\"raw.sales\"], mock_superset)\n assert 101 in result\n assert 102 not in result\n # #endregion test_table_based_match\n\n # #region test_virtual_dataset_match [C:2] [TYPE Function]\n # @BRIEF Virtual SQL dataset matching via SqlTableExtractor.\n def test_virtual_dataset_match(self, mock_superset):\n mock_superset.get_datasets.return_value = (1, [\n {\n \"id\": 1,\n \"schema\": \"raw\",\n \"table_name\": \"sales_view\",\n \"sql\": \"SELECT * FROM raw.sales\",\n \"is_sqllab_view\": True,\n },\n ])\n mock_superset.get_dataset_detail.return_value = {\n \"linked_dashboards\": [{\"id\": 101}],\n }\n\n result = find_affected_dashboards([\"raw.sales\"], mock_superset)\n assert 101 in result\n # #endregion test_virtual_dataset_match\n\n # #region test_case_insensitive_matching [C:2] [TYPE Function]\n # @BRIEF Matching is case-insensitive.\n def test_case_insensitive_matching(self, mock_superset):\n mock_superset.get_datasets.return_value = (1, [\n {\"id\": 1, \"schema\": \"RAW\", \"table_name\": \"SALES\", \"sql\": None},\n ])\n mock_superset.get_dataset_detail.return_value = {\n \"linked_dashboards\": [{\"id\": 101}],\n }\n\n result = find_affected_dashboards([\"raw.sales\"], mock_superset)\n assert 101 in result\n # #endregion test_case_insensitive_matching\n\n # #region test_superset_api_failure [C:2] [TYPE Function]\n # @BRIEF Superset API failure propagates.\n def test_superset_api_failure(self, mock_superset):\n mock_superset.get_datasets.side_effect = Exception(\"Superset API error\")\n with pytest.raises(Exception, match=\"Superset API error\"):\n find_affected_dashboards([\"raw.sales\"], mock_superset)\n # #endregion test_superset_api_failure\n\n\n# ── T019a: ensure_banner_chart tests ──────────────────────────\n\nclass TestEnsureBannerChart:\n \"\"\"Tests for ensure_banner_chart.\"\"\"\n\n # #region test_creates_new_banner [C:2] [TYPE Function]\n # @BRIEF No existing banner — creates new chart and banner row.\n def test_creates_new_banner(self, mock_superset, db_session):\n banner = ensure_banner_chart(\n 101, \"test-env\", mock_superset, db_session\n )\n assert banner.dashboard_id == 101\n assert banner.chart_id == 12345\n assert banner.status == MaintenanceDashboardBannerStatus.ACTIVE\n mock_superset.create_markdown_chart.assert_called_once_with(101, \"*Maintenance banner placeholder*\")\n mock_superset.update_dashboard_layout.assert_called_once_with(101, 12345)\n # #endregion test_creates_new_banner\n\n # #region test_returns_existing_banner [C:2] [TYPE Function]\n # @BRIEF Existing active banner — returns it.\n def test_returns_existing_banner(self, mock_superset, db_session):\n existing = MaintenanceDashboardBanner(\n environment_id=\"test-env\",\n dashboard_id=101,\n chart_id=999,\n banner_text=\"Existing\",\n status=MaintenanceDashboardBannerStatus.ACTIVE,\n )\n db_session.add(existing)\n db_session.commit()\n\n banner = ensure_banner_chart(\n 101, \"test-env\", mock_superset, db_session\n )\n assert banner.id == existing.id\n assert banner.chart_id == 999\n mock_superset.create_markdown_chart.assert_not_called()\n # #endregion test_returns_existing_banner\n\n # #region test_chart_creation_failure [C:2] [TYPE Function]\n # @BRIEF Chart creation failure propagates.\n def test_chart_creation_failure(self, mock_superset, db_session):\n mock_superset.create_markdown_chart.side_effect = Exception(\"Chart error\")\n with pytest.raises(Exception, match=\"Chart error\"):\n ensure_banner_chart(101, \"test-env\", mock_superset, db_session)\n # #endregion test_chart_creation_failure\n\n\n# ── T020: build_banner_text tests ─────────────────────────────\n\nclass TestBuildBannerText:\n \"\"\"Tests for build_banner_text.\"\"\"\n\n # #region test_single_event [C:2] [TYPE Function]\n # @BRIEF Single event — direct substitution.\n def test_single_event(self):\n events = [\n {\"start_time\": \"2026-05-21T22:00:00+00:00\",\n \"end_time\": \"2026-05-22T02:00:00+00:00\",\n \"message\": \"Updating tables\"}\n ]\n template = \"Banner: {message} ({start_time} - {end_time})\"\n result = build_banner_text(events, template)\n assert \"Updating tables\" in result\n assert \"2026-05-21 22:00\" in result\n assert \"2026-05-22 02:00\" in result\n # #endregion test_single_event\n\n # #region test_multiple_events [C:2] [TYPE Function]\n # @BRIEF Multiple events — combined message.\n def test_multiple_events(self):\n events = [\n {\"start_time\": \"2026-05-21T22:00:00+00:00\",\n \"end_time\": \"2026-05-22T02:00:00+00:00\",\n \"message\": \"Event 1\"},\n {\"start_time\": \"2026-05-23T22:00:00+00:00\",\n \"end_time\": \"2026-05-24T02:00:00+00:00\",\n \"message\": \"Event 2\"},\n ]\n template = \"Banner: {message} ({start_time} - {end_time})\"\n result = build_banner_text(events, template)\n assert \"Event 1\" in result\n assert \"Event 2\" in result\n # #endregion test_multiple_events\n\n # #region test_empty_events [C:2] [TYPE Function]\n # @BRIEF Empty events list returns empty string.\n def test_empty_events(self):\n result = build_banner_text([], \"Template: {message}\")\n assert result == \"\"\n # #endregion test_empty_events\n\n # #region test_missing_variables [C:2] [TYPE Function]\n # @BRIEF Missing variables replaced with empty string.\n def test_missing_variables(self):\n events = [{\"start_time\": None, \"end_time\": None, \"message\": \"Hi\"}]\n template = \"Start: {start_time} - End: {end_time} - Msg: {message}\"\n result = build_banner_text(events, template)\n assert \"уточняется\" in result\n assert \"Hi\" in result\n # #endregion test_missing_variables\n\n # #region test_html_escaping [C:2] [TYPE Function]\n # @BRIEF Message is HTML-escaped.\n def test_html_escaping(self):\n events = [{\n \"start_time\": \"2026-05-21T22:00:00+00:00\",\n \"end_time\": \"2026-05-22T02:00:00+00:00\",\n \"message\": \"\",\n }]\n template = \"{message}\"\n result = build_banner_text(events, template)\n assert \"\",\n }]\n template = \"{message}\"\n result = build_banner_text(events, template)\n assert \"\n\n\n
\n {#each $toasts as toast (toast.id)}\n
\n {toast.message}\n
\n {/each}\n
\n\n\n\n" + "body": "\n\n\n\n\n\n\n
\n {#each $toasts.filter(t => t.persistent) as toast (toast.id)}\n \n
\n {#if toast.type === 'warning'}\n \n {:else if toast.type === 'error'}\n \n {:else if toast.type === 'success'}\n \n {:else}\n \n {/if}\n
\n
\n {toast.message}\n
\n handleDismiss(toast.id)}\n class=\"flex-shrink-0 rounded p-1 transition-colors\n {toast.type === 'warning' ? 'hover:bg-amber-100 text-amber-400' : ''}\n {toast.type === 'error' ? 'hover:bg-red-100 text-red-400' : ''}\n {toast.type === 'info' ? 'hover:bg-blue-100 text-blue-400' : ''}\n {toast.type === 'success' ? 'hover:bg-green-100 text-green-400' : ''}\n \"\n aria-label=\"Закрыть\"\n >\n \n \n
\n {/each}\n\n\n\n
\n {#each $toasts.filter(t => !t.persistent) as toast (toast.id)}\n
\n {toast.message}\n
\n {/each}\n
\n\n\n\n" }, { "contract_id": "TaskLogViewerTest:Module", @@ -60950,8 +61934,8 @@ "contract_id": "handleCreateBackup:Function", "contract_type": "Function", "file_path": "frontend/src/components/backups/BackupManager.svelte", - "start_line": 106, - "end_line": 167, + "start_line": 115, + "end_line": 176, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -60971,8 +61955,8 @@ "contract_id": "handleUpdateSchedule:Function", "contract_type": "Function", "file_path": "frontend/src/components/backups/BackupManager.svelte", - "start_line": 117, - "end_line": 147, + "start_line": 126, + "end_line": 156, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -60991,7 +61975,7 @@ "contract_type": "Component", "file_path": "frontend/src/components/git/BranchSelector.svelte", "start_line": 1, - "end_line": 168, + "end_line": 202, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -61030,7 +62014,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n ({ value: b.name, label: b.name }))}\n />\n
\n\n showCreate = !showCreate}\n disabled={loading}\n class=\"text-primary\"\n >\n + {$t.git.new_branch}\n \n
\n\n {#if showCreate}\n
\n
\n \n
\n \n {$t.git.create}\n \n showCreate = false}\n disabled={loading}\n >\n {$t.common.cancel}\n \n
\n {/if}\n
\n\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n ({ value: b.name, label: b.name }))}\n />\n
\n\n showCreate = !showCreate}\n disabled={loading}\n class=\"text-primary\"\n >\n + {$t.git.new_branch}\n \n
\n\n {#if branchError}\n
\n
\n \n
\n

{branchError.message}

\n {#if branchError.next_steps?.length}\n
    \n {#each branchError.next_steps as step}\n
  • {step}
  • \n {/each}\n
\n {/if}\n
\n \n
\n
\n {/if}\n\n {#if showCreate}\n
\n
\n \n
\n \n {$t.git.create}\n \n showCreate = false}\n disabled={loading}\n >\n {$t.common.cancel}\n \n
\n {/if}\n
\n\n\n\n" }, { "contract_id": "props", @@ -61055,7 +62039,7 @@ "contract_type": "Block", "file_path": "frontend/src/components/git/BranchSelector.svelte", "start_line": 30, - "end_line": 35, + "end_line": 36, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -61065,14 +62049,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region state [C:1] [TYPE Block]\n let branches = $state([]);\n let loading = $state(false);\n let showCreate = $state(false);\n let newBranchName = $state('');\n // #endregion state\n" + "body": " // #region state [C:1] [TYPE Block]\n let branches = $state([]);\n let loading = $state(false);\n let showCreate = $state(false);\n let newBranchName = $state('');\n let branchError = $state(null);\n // #endregion state\n" }, { "contract_id": "loadBranches", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 37, - "end_line": 55, + "start_line": 38, + "end_line": 56, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61092,8 +62076,8 @@ "contract_id": "handleSelect", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 57, - "end_line": 64, + "start_line": 58, + "end_line": 65, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -61112,8 +62096,8 @@ "contract_id": "handleCheckout", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 66, - "end_line": 84, + "start_line": 67, + "end_line": 95, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61121,20 +62105,21 @@ "COMPLEXITY": 2, "POST": "currentBranch updated; onchange callback fired.", "PRE": "branchName is non-empty; envId required when ref is slug.", - "SIDE_EFFECT": "POSTs to /git/repositories/{ref}/checkout." + "SIDE_EFFECT": "POSTs to /git/repositories/{ref}/checkout.", + "UX_STATE": "Error -> Inline error message below the dropdown; persistent toast above modal with full details." }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleCheckout [C:2] [TYPE Function]\n // @BRIEF Switch repository to selected branch.\n // @PRE branchName is non-empty; envId required when ref is slug.\n // @POST currentBranch updated; onchange callback fired.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/checkout.\n async function handleCheckout(branchName) {\n console.log(`[EXT:frontend:BranchSelector][Action] Checking out branch ${branchName}`);\n try {\n await gitService.checkoutBranch(dashboardId, branchName, envId);\n currentBranch = branchName;\n onchange({ branch: branchName });\n toast($t.git?.switched_to?.replace('{branch}', branchName), 'success');\n console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Checked out ${branchName}`);\n } catch (e) {\n console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // #endregion handleCheckout\n" + "body": " // #region handleCheckout [C:2] [TYPE Function]\n // @BRIEF Switch repository to selected branch.\n // @PRE branchName is non-empty; envId required when ref is slug.\n // @POST currentBranch updated; onchange callback fired.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/checkout.\n // @UX_STATE Error -> Inline error message below the dropdown; persistent toast above modal with full details.\n async function handleCheckout(branchName) {\n branchError = null;\n console.log(`[EXT:frontend:BranchSelector][Action] Checking out branch ${branchName}`);\n try {\n await gitService.checkoutBranch(dashboardId, branchName, envId);\n currentBranch = branchName;\n onchange({ branch: branchName });\n toast($t.git?.switched_to?.replace('{branch}', branchName), 'success');\n console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Checked out ${branchName}`);\n } catch (e) {\n console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`);\n const detail = typeof e?.detail === 'object' ? e.detail : {};\n const nextSteps = detail?.next_steps || [];\n const shortMsg = detail?.message || detail?.message_en || 'Ошибка переключения ветки';\n branchError = {\n message: shortMsg,\n next_steps: nextSteps,\n };\n // Persistent toast above modal with the full technical error\n toast(shortMsg, 'warning', 0);\n }\n }\n // #endregion handleCheckout\n" }, { "contract_id": "handleCreate", "contract_type": "Function", "file_path": "frontend/src/components/git/BranchSelector.svelte", - "start_line": 86, - "end_line": 106, + "start_line": 97, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61148,7 +62133,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleCreate [C:2] [TYPE Function]\n // @BRIEF Create a new branch from current branch.\n // @PRE newBranchName is non-empty; envId required when ref is slug.\n // @POST New branch created; branch list reloaded; create mode closed.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/branches.\n async function handleCreate() {\n if (!newBranchName) return;\n console.log(`[EXT:frontend:BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);\n try {\n await gitService.createBranch(dashboardId, newBranchName, currentBranch, envId);\n toast($t.git?.created_branch?.replace('{branch}', newBranchName), 'success');\n showCreate = false;\n newBranchName = '';\n await loadBranches();\n console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Branch created`);\n } catch (e) {\n console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`);\n toast(e.message, 'error');\n }\n }\n // #endregion handleCreate\n" + "body": " // #region handleCreate [C:2] [TYPE Function]\n // @BRIEF Create a new branch from current branch.\n // @PRE newBranchName is non-empty; envId required when ref is slug.\n // @POST New branch created; branch list reloaded; create mode closed.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/branches.\n async function handleCreate() {\n if (!newBranchName) return;\n branchError = null;\n console.log(`[EXT:frontend:BranchSelector][Action] Creating branch ${newBranchName} from ${currentBranch}`);\n try {\n await gitService.createBranch(dashboardId, newBranchName, currentBranch, envId);\n toast($t.git?.created_branch?.replace('{branch}', newBranchName), 'success');\n showCreate = false;\n newBranchName = '';\n await loadBranches();\n console.log(`[EXT:frontend:BranchSelector][Coherence:OK] Branch created`);\n } catch (e) {\n console.error(`[EXT:frontend:BranchSelector][Coherence:Failed] ${e.message}`);\n branchError = { message: e.message || 'Ошибка создания ветки', next_steps: [] };\n toast(e.message, 'error');\n }\n }\n // #endregion handleCreate\n" }, { "contract_id": "loadHistory:Function", @@ -61344,14 +62329,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n
\n \n

\n {$t.git?.not_linked || 'Этот дашборд еще не привязан к Git-репозиторию.'}\n

\n\n
\n ({ value: c.id, label: `${c.name} (${c.provider})` }))}\n />\n {#if configs.length === 0}\n

{$t.git?.no_servers_configured || 'Нет сконфигурированных Git серверов'}

\n {/if}\n\n \n \n Create repo\n \n\n \n {$t.git?.init_repo || 'Инициализировать Git-репозиторий'}\n \n
\n
\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n
\n \n

\n {$t.git?.not_linked || 'Этот дашборд еще не привязан к Git-репозиторию.'}\n

\n\n
\n ({ value: c.id, label: `${c.name} (${c.provider})` }))}\n />\n {#if configs.length === 0}\n

{$t.git?.no_servers_configured || 'Нет сконфигурированных Git серверов'}

\n {/if}\n\n \n \n Create repo\n \n\n \n {$t.git?.init_repo || 'Инициализировать Git-репозиторий'}\n \n
\n
\n
\n\n" }, { "contract_id": "GitManager", "contract_type": "Component", "file_path": "frontend/src/components/git/GitManager.svelte", "start_line": 1, - "end_line": 231, + "end_line": 330, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -61360,7 +62345,9 @@ "LAYER": "UI", "RATIONALE": "Decomposed from 1220→~370 lines by extracting sub-panels, utils, and composable handlers per INV_7.", "REJECTED": "Keeping all logic inline was rejected because it exceeded 150-line contract limit.", - "UX_STATE": "MergeDialog -> Recovery overlay." + "UX_FEEDBACK": "Toast is still shown for quick notification alongside the persistent banner.", + "UX_RECOVERY": "Dismiss error, Retry operation after fixing the issue.", + "UX_STATE": "Error -> Red banner with error details and dismiss action." }, "relations": [ { @@ -61433,7 +62420,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n{#if show}\n
{ if (e.key === 'Escape') closeModal(); }} role=\"button\" tabindex=\"0\" aria-label={$t.common?.close || 'Close'}>\n
\n \n \n
slug: {dashboardId}
\n
\n\n {#if checkingStatus}\n
\n {:else if !initialized}\n \n {:else}\n
\n {#if hasOriginConfigMismatch}\n
Git server mismatch detected
Configured: {configHost}, origin: {originHost}.
\n {/if}\n
\n
\n {currentEnvStage || 'DEV'}\n Текущая ветка: {currentBranch}\n
\n
\n
\n
\n \n \n \n
\n {#if activeTab === 'workspace'}\n \n {:else if activeTab === 'release'}\n \n {:else}\n \n {/if}\n
\n {/if}\n
\n \n
\n{/if}\n\n\n\n" + "body": "\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\n{#if show}\n \n
{ if (e.key === 'Escape') closeModal(); }} role=\"dialog\" tabindex=\"-1\" aria-label={`${$t.git?.management || 'Управление Git'}: ${dashboardTitle}`}>\n
\n \n
\n
\n
\n \n
\n
\n

{$t.git?.management || 'Управление Git'}

\n

{dashboardTitle} · slug: {dashboardId}

\n
\n
\n \n
\n\n \n {#if gitError}\n \n
\n {#if gitErrorType === 'warning'}\n \n {:else}\n \n {/if}\n
\n
\n

{gitError.message || gitError}

\n {#if gitError.files?.length}\n
\n Файлы, которые будут перезаписаны ({gitError.files.length})\n
    \n {#each gitError.files as file}\n
  • {file}
  • \n {/each}\n
\n
\n {/if}\n {#if gitError.next_steps?.length}\n
\n Рекомендации:\n
    \n {#each gitError.next_steps as step}\n
  1. {step}
  2. \n {/each}\n
\n
\n {/if}\n
\n { gitError = null; gitErrorType = 'error'; }}\n class=\"flex-shrink-0 rounded p-1 transition-colors {gitErrorType === 'warning' ? 'hover:bg-amber-100' : 'hover:bg-red-100'}\"\n aria-label=\"Закрыть\"\n >\n \n \n
\n {/if}\n\n \n
\n\n {#if checkingStatus}\n
\n {:else if !initialized}\n \n {:else}\n
\n {#if hasOriginConfigMismatch}\n
Git server mismatch detected
Configured: {configHost}, origin: {originHost}.
\n {/if}\n
\n
\n \n \n {currentEnvStage || 'DEV'}\n \n ·\n \n \n Ветка: {currentBranch}\n \n {#if changedFilesCount > 0}\n \n \n {changedFilesCount} изменений\n \n {/if}\n
\n
\n
\n
\n \n \n \n
\n {#if activeTab === 'workspace'}\n \n {:else if activeTab === 'release'}\n \n {:else}\n \n {/if}\n
\n {/if}\n
\n\n \n
\n\n{/if}\n\n\n\n" }, { "contract_id": "GitMergeDialog", @@ -61525,13 +62512,15 @@ "contract_type": "Component", "file_path": "frontend/src/components/git/GitWorkspacePanel.svelte", "start_line": 1, - "end_line": 96, + "end_line": 295, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "BRIEF": "Git workspace panel: sync, commit message input, diff viewer, commit action.", - "COMPLEXITY": 3, + "BRIEF": "Git workspace panel: sync, commit, lazy-chunked diff viewer (IntersectionObserver).", + "COMPLEXITY": 4, "LAYER": "UI", + "PRE": "hasWorkspaceChanges and workspaceDiff are propagated from GitManager.", + "UX_RECOVERY": "Scroll down to load more diff chunks; sentinel triggers lazy render.", "UX_STATE": "Error -> Toast on failure." }, "relations": [ @@ -61551,7 +62540,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n
\n
\n \n 🔄 Синхронизировать из Superset\n \n\n
\n
\n

Сообщение коммита

\n \n ✨ Сгенерировать LLM\n \n
\n \n
\n\n
\n Файлов с изменениями: {changedFilesCount}\n
\n\n \n Зафиксировать (Commit)\n \n \n
\n\n
\n
\n Diff (изменения)\n
\n
\n {#if workspaceLoading}\n
\n {#each Array(6) as _}\n
\n {/each}\n
\n {:else}\n
\n Нет изменений для коммита\n
\n {/if}\n
\n
\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n \n
\n \n \n Синхронизировать из Superset\n \n
\n\n \n
\n
\n

Сообщение коммита

\n \n \n LLM\n \n
\n \n
\n\n \n
\n
\n \n Файлов с изменениями: {changedFilesCount}\n
\n\n \n \n Зафиксировать (Commit)\n \n\n \n
\n
\n\n \n
\n
\n
\n \n Diff (изменения)\n
\n {#if hasWorkspaceChanges}\n \n {changedFilesCount} файлов\n {#if totalChunks > 0}\n · {renderedChunkCount}/{totalChunks}\n {/if}\n \n {/if}\n
\n
\n {#if workspaceLoading}\n
\n {#each Array(8) as _}\n
\n {/each}\n
\n {#each Array(6) as _}\n
\n {/each}\n
\n
\n {:else if hasWorkspaceChanges && renderedHtml}\n
{@html renderedHtml}
\n \n {#if hasMoreChunks}\n \n {#if chunkLoading}\n
\n
\n {$t.git?.diff_loading || 'Загрузка изменений...'}\n
\n {:else}\n {\n renderedChunkCount = Math.min(renderedChunkCount + CHUNKS_PER_PAGE, totalChunks);\n }}>\n {$t.git?.diff_show_more?.replace('{count}', totalChunks - renderedChunkCount) || `Показать ещё (${totalChunks - renderedChunkCount} файлов)`}\n \n {/if}\n
\n {:else if totalChunks > 0}\n
\n {$t.git?.diff_all_shown?.replace('{count}', totalChunks) || `Показаны все изменения (${totalChunks} файлов)`}\n
\n {/if}\n {:else if hasWorkspaceChanges}\n
\n \n Загрузка diff... (нажмите «Синхронизировать»)\n
\n {:else}\n
\n \n Нет изменений для коммита\n Синхронизируйте дашборд, чтобы увидеть изменения\n
\n {/if}\n
\n
\n\n\n\n\n\n" }, { "contract_id": "GitManagerUnfinishedMergeIntegrationTest:Module", @@ -61581,14 +62570,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region GitManagerUnfinishedMergeIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @RELATION DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst MANAGER_PATH = path.resolve(process.cwd(), 'src/components/git/GitManager.svelte');\nconst UTILS_PATH = path.resolve(process.cwd(), 'src/services/git-utils.js');\nconst HANDLER_PATH = path.resolve(process.cwd(), 'src/components/git/useGitManager.js');\nconst DIALOG_PATH = path.resolve(process.cwd(), 'src/components/git/GitMergeDialog.svelte');\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const utils = fs.readFileSync(UTILS_PATH, 'utf-8');\n const handlers = fs.readFileSync(HANDLER_PATH, 'utf-8');\n const manager = fs.readFileSync(MANAGER_PATH, 'utf-8');\n\n expect(utils).toContain(\"error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(handlers).toContain('showUnfinishedMergeDialog: true');\n expect(handlers).toContain('this.openUnfinishedMergeDialogFromError(e)');\n expect(handlers).toContain('await this.loadMergeRecoveryState();');\n expect(manager).toContain('h.handlePull');\n expect(manager).toContain('h.loadMergeRecoveryState');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(DIALOG_PATH, 'utf-8');\n\n expect(source).toContain('{#if show && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('onCopyCommands');\n expect(source).toContain('onOpenConflictResolver');\n expect(source).toContain('onAbortMerge');\n expect(source).toContain('onContinueMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// #endregion GitManagerUnfinishedMergeIntegrationTest:Module\n" + "body": "// #region GitManagerUnfinishedMergeIntegrationTest:Module [TYPE Function]\n// @SEMANTICS: git-manager, unfinished-merge, dialog, integration-test\n// @PURPOSE: Protect unresolved-merge dialog contract in GitManager pull flow.\n// @RELATION DEPENDS_ON -> [GitManager]\n\nimport { describe, it, expect } from 'vitest';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst MANAGER_PATH = path.resolve(process.cwd(), 'src/components/git/GitManager.svelte');\nconst UTILS_PATH = path.resolve(process.cwd(), 'src/services/git-utils.js');\nconst HANDLER_PATH = path.resolve(process.cwd(), 'src/components/git/useGitManager.js');\nconst DIALOG_PATH = path.resolve(process.cwd(), 'src/components/git/GitMergeDialog.svelte');\n\ndescribe('GitManager unfinished merge dialog contract', () => {\n it('keeps 409 unfinished-merge detection and WebUI dialog recovery flow in pull handler', () => {\n const utils = fs.readFileSync(UTILS_PATH, 'utf-8');\n const handlers = fs.readFileSync(HANDLER_PATH, 'utf-8');\n const manager = fs.readFileSync(MANAGER_PATH, 'utf-8');\n\n expect(utils).toContain(\"error_code !== 'GIT_UNFINISHED_MERGE'\");\n expect(handlers).toContain('showUnfinishedMergeDialog: true');\n expect(handlers).toContain('handlers.openUnfinishedMergeDialogFromError(e)');\n expect(handlers).toContain('await handlers.loadMergeRecoveryState();');\n expect(manager).toContain('h.handlePull');\n expect(manager).toContain('h.loadMergeRecoveryState');\n });\n\n it('renders unresolved-merge dialog details and web recovery actions', () => {\n const source = fs.readFileSync(DIALOG_PATH, 'utf-8');\n\n expect(source).toContain('{#if show && unfinishedMergeContext}');\n expect(source).toContain('unfinishedMergeContext.repositoryPath');\n expect(source).toContain('unfinishedMergeContext.currentBranch');\n expect(source).toContain('unfinishedMergeContext.commands.join');\n expect(source).toContain('onCopyCommands');\n expect(source).toContain('onOpenConflictResolver');\n expect(source).toContain('onAbortMerge');\n expect(source).toContain('onContinueMerge');\n expect(source).toContain('$t.git?.unfinished_merge?.copy_commands');\n });\n});\n// #endregion GitManagerUnfinishedMergeIntegrationTest:Module\n" }, { "contract_id": "UseGitManager", "contract_type": "Module", "file_path": "frontend/src/components/git/useGitManager.js", "start_line": 1, - "end_line": 400, + "end_line": 477, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -61613,14 +62602,34 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region UseGitManager [C:3] [TYPE Module] [SEMANTICS git, handlers, composable, orchestration]\n// @BRIEF Composable for GitManager handler functions — extracted to reduce component size per INV_7.\n// @RELATION DEPENDS_ON -> [EXT:frontend:gitService]\n// @RELATION DEPENDS_ON -> [GitUtils]\n// @RATIONALE Extracted handler logic from GitManager (1220→~350 lines) to meet INV_7 <150 line contract limit.\nimport { gitService } from '../../services/gitService';\nimport { api } from '../../lib/api.js';\nimport { isNumericDashboardRef, resolveDefaultConfig, extractUnfinishedMergeContext, buildSuggestedRepoName } from '../../services/git-utils.js';\n\nexport function createGitHandlers(getState, setState) {\n const s = () => getState();\n const u = (patch) => setState(patch);\n\n return {\n\n // #region loadCurrentEnvironmentStage [C:2] [TYPE Function]\n // @BRIEF Resolve current environment stage (DEV/PREPROD/PROD) and apply GitFlow defaults.\n // @PRE resolveEnvId() returns a valid environment ID, or handler exits silently.\n // @POST currentEnvStage and GitFlow defaults (promoteFromBranch, promoteToBranch) are set.\n // @SIDE_EFFECT Fetches /environments from API.\n async loadCurrentEnvironmentStage() {\n const currentEnvId = s().resolveEnvId();\n if (!currentEnvId) return;\n try {\n const environments = await api.getEnvironmentsList();\n const currentEnv = (environments || []).find((item) => item.id === currentEnvId);\n if (!currentEnv) return;\n const stage = s().normalizeEnvStageFn(currentEnv);\n u({ currentEnvStage: stage });\n const defaults = s().applyGitflowStageDefaultsFn(stage);\n u(defaults);\n } catch (e) {\n console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);\n }\n },\n // #endregion loadCurrentEnvironmentStage\n\n // #region checkStatus [C:3] [TYPE Function]\n // @BRIEF Check if Git repository is initialized for the dashboard.\n // @PRE dashboardId is a non-numeric slug; envId is required (resolved with localStorage fallback).\n // @POST initialized=true if repository found and workspace loaded; initialized=false if not found or envId missing.\n // @SIDE_EFFECT Fetches /git/repositories/{ref}/branches; may fetch /git/repositories/{ref}/status and /diff on success.\n async checkStatus() {\n if (isNumericDashboardRef(s().dashboardId)) {\n u({ checkingStatus: false, initialized: false });\n s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n u({ checkingStatus: true });\n if (!s().envId) {\n u({ initialized: false, checkingStatus: false });\n return;\n }\n try {\n await gitService.getBranches(s().dashboardId, s().envId);\n u({ initialized: true });\n await this.loadWorkspace();\n } catch (_e) {\n u({ initialized: false });\n } finally { u({ checkingStatus: false }); }\n },\n // #endregion checkStatus\n\n // #region loadWorkspace [C:2] [TYPE Function]\n // @BRIEF Load Git workspace status and diff for initialized repository.\n // @PRE initialized is true; envId is required when ref is slug.\n // @POST workspaceStatus and workspaceDiff are updated; currentBranch synced from server.\n // @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.\n async loadWorkspace() {\n if (!s().initialized) return;\n u({ workspaceLoading: true });\n try {\n const ws = await gitService.getStatus(s().dashboardId, s().envId);\n const sd = await gitService.getDiff(s().dashboardId, null, true, s().envId);\n const ud = await gitService.getDiff(s().dashboardId, null, false, s().envId);\n u({ workspaceStatus: ws, workspaceDiff: [sd, ud].filter(Boolean).join('\\n\\n'), currentBranch: ws?.current_branch || s().currentBranch });\n } catch (e) {\n s().toast(e.message || 'Не удалось загрузить изменения', 'error');\n } finally { u({ workspaceLoading: false }); }\n },\n // #endregion loadWorkspace\n\n // #region handleSync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @PRE dashboardId is a non-numeric slug; envId required when ref is slug.\n // @POST Dashboard state synced to Git workspace; workspace reloaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync; reloads workspace on success.\n async handleSync() {\n if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }\n u({ loading: true });\n try {\n const sourceEnvId = localStorage.getItem('selected_env_id');\n await gitService.sync(s().dashboardId, sourceEnvId, s().envId);\n s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n // #endregion handleSync\n\n // #region handleGenerateMessage [C:2] [TYPE Function]\n // @BRIEF Generate AI commit message from dashboard diff.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.\n // @POST commitMessage state updated with generated text.\n async handleGenerateMessage() {\n u({ generatingMessage: true });\n try {\n const data = await api.postApi(`/git/repositories/${encodeURIComponent(String(s().dashboardId))}/generate-message${s().envId ? `?env_id=${encodeURIComponent(String(s().envId))}` : ''}`, undefined, { suppressToast: true });\n u({ commitMessage: data?.message || '' });\n s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');\n } catch (e) { s().toast(e.message || s().t?.git?.commit_message_failed || 'Не удалось сгенерировать сообщение', 'error'); } finally { u({ generatingMessage: false }); }\n },\n // #endregion handleGenerateMessage\n\n // #region handleCommit [C:2] [TYPE Function]\n // @BRIEF Stage and commit workspace changes, optionally push.\n // @PRE commitMessage is non-empty; hasWorkspaceChanges is true; envId required when ref is slug.\n // @POST Changes committed (and pushed if autoPushAfterCommit); workspace reloaded.\n // @SIDE_EFFECT POSTs to /commit and optionally /push.\n async handleCommit() {\n if (!s().commitMessage || !s().hasWorkspaceChanges) return;\n u({ committing: true });\n try {\n await gitService.commit(s().dashboardId, s().commitMessage, [], s().envId);\n if (s().autoPushAfterCommit) {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');\n } else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }\n u({ commitMessage: '' });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ committing: false }); }\n },\n // #endregion handleCommit\n\n // #region openUnfinishedMergeDialogFromError [C:2] [TYPE Function]\n // @BRIEF Parse error for unfinished merge context and open dialog.\n // @PRE error contains GIT_UNFINISHED_MERGE error_code or related detail.\n // @POST Returns true and opens dialog if merge context found; false otherwise.\n openUnfinishedMergeDialogFromError(error) {\n const context = extractUnfinishedMergeContext(error);\n if (!context) return false;\n u({ unfinishedMergeContext: context, showUnfinishedMergeDialog: true });\n return true;\n },\n // #endregion openUnfinishedMergeDialogFromError\n\n // #region loadMergeRecoveryState [C:2] [TYPE Function]\n // @BRIEF Load merge recovery state from backend for unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST unfinishedMergeContext is populated; dialog closed if no merge in progress.\n // @SIDE_EFFECT GETs /merge/status from backend.\n async loadMergeRecoveryState() {\n u({ mergeRecoveryLoading: true });\n try {\n const status = await gitService.getMergeStatus(s().dashboardId, s().envId);\n if (!status?.has_unfinished_merge) { this.closeUnfinishedMergeDialog(); return; }\n u({\n unfinishedMergeContext: {\n ...(s().unfinishedMergeContext || {}),\n message: s().unfinishedMergeContext?.message || (s().t?.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || s().unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || s().unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || s().unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || s().unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || ''),\n nextSteps: Array.isArray(s().unfinishedMergeContext?.nextSteps) ? s().unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(s().unfinishedMergeContext?.commands) ? s().unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n }\n });\n } catch (e) { s().toast(e.message || 'Failed to load merge status', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion loadMergeRecoveryState\n\n // #region closeUnfinishedMergeDialog [C:1] [TYPE Function]\n // @BRIEF Close unfinished merge dialog and reset merge state.\n // @POST Dialog hidden; mergeConflicts, showConflictResolver reset.\n closeUnfinishedMergeDialog() {\n u({ showUnfinishedMergeDialog: false, unfinishedMergeContext: null, mergeConflicts: [], showConflictResolver: false });\n },\n // #endregion closeUnfinishedMergeDialog\n\n // #region handleOpenConflictResolver [C:2] [TYPE Function]\n // @BRIEF Load merge conflicts and open conflict resolver UI.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST mergeConflicts populated; showConflictResolver set to true if conflicts found.\n // @SIDE_EFFECT GETs /merge/conflicts from backend.\n async handleOpenConflictResolver() {\n u({ mergeRecoveryLoading: true });\n try {\n const mc = await gitService.getMergeConflicts(s().dashboardId, s().envId);\n if (!Array.isArray(mc) || mc.length === 0) { s().toast(s().t?.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info'); return; }\n u({ mergeConflicts: mc, showConflictResolver: true });\n } catch (e) { s().toast(e.message || 'Failed to load merge conflicts', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion handleOpenConflictResolver\n\n // #region handleResolveConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolutions and stage resolved files.\n // @PRE event.detail maps file paths to resolution strategies; envId required when ref is slug.\n // @POST Conflicts resolved and staged; resolver closed; workspace reloaded.\n async handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));\n if (!resolutions.length) { s().toast(s().t?.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning'); return; }\n u({ mergeResolveInProgress: true });\n try {\n await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n u({ showConflictResolver: false });\n await this.loadMergeRecoveryState();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to resolve conflicts', 'error'); } finally { u({ mergeResolveInProgress: false }); }\n },\n // #endregion handleResolveConflicts\n\n // #region handleAbortUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE envId required when ref is slug.\n // @POST Merge aborted; dialog closed; workspace reloaded.\n async handleAbortUnfinishedMerge() {\n u({ mergeAbortInProgress: true });\n try {\n await gitService.abortMerge(s().dashboardId, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to abort merge', 'error'); } finally { u({ mergeAbortInProgress: false }); }\n },\n // #endregion handleAbortUnfinishedMerge\n\n // #region handleContinueUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating merge commit.\n // @PRE All conflicts resolved; envId required when ref is slug.\n // @POST Merge commit created; dialog closed; workspace reloaded.\n async handleContinueUnfinishedMerge() {\n u({ mergeContinueInProgress: true });\n try {\n await gitService.continueMerge(s().dashboardId, '', s().envId);\n s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) {\n s().toast(e.message || 'Failed to continue merge', 'error');\n await this.loadMergeRecoveryState();\n } finally { u({ mergeContinueInProgress: false }); }\n },\n // #endregion handleContinueUnfinishedMerge\n\n // #region getUnfinishedMergeCommandsText [C:1] [TYPE Function]\n // @BRIEF Get shell commands for manual merge recovery.\n // @POST Returns concatenated command string or empty string.\n getUnfinishedMergeCommandsText() {\n if (!s().unfinishedMergeContext?.commands?.length) return '';\n return s().unfinishedMergeContext.commands.join('\\n');\n },\n // #endregion getUnfinishedMergeCommandsText\n\n // #region handleCopyUnfinishedMergeCommands [C:1] [TYPE Function]\n // @BRIEF Copy merge recovery commands to clipboard.\n // @PRE commands exist in unfinishedMergeContext.\n // @POST Commands copied to clipboard; toast shown.\n async handleCopyUnfinishedMergeCommands() {\n const text = this.getUnfinishedMergeCommandsText();\n if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }\n u({ copyingUnfinishedMergeCommands: true });\n try {\n await navigator.clipboard.writeText(text);\n s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }\n },\n // #endregion handleCopyUnfinishedMergeCommands\n\n // #region handlePull [C:2] [TYPE Function]\n // @BRIEF Pull changes from remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Local repository updated with remote changes; workspace reloaded.\n // @SIDE_EFFECT POSTs to /pull; may open merge dialog on GIT_UNFINISHED_MERGE.\n async handlePull() {\n u({ isPulling: true });\n try {\n await gitService.pull(s().dashboardId, s().envId);\n s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');\n await this.loadWorkspace();\n } catch (e) {\n const handled = this.openUnfinishedMergeDialogFromError(e);\n if (handled) await this.loadMergeRecoveryState();\n else s().toast(e.message, 'error');\n } finally { u({ isPulling: false }); }\n },\n // #endregion handlePull\n\n // #region handlePush [C:2] [TYPE Function]\n // @BRIEF Push local commits to remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Remote updated with local commits; workspace reloaded.\n async handlePush() {\n u({ isPushing: true });\n try {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ isPushing: false }); }\n },\n // #endregion handlePush\n\n // #region handlePromote [C:3] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE promoteFromBranch and promoteToBranch differ; direct mode requires reason; envId required when ref is slug.\n // @POST MR created on Git server (MR mode) or direct merge executed (direct mode).\n // @SIDE_EFFECT Opens MR URL in new tab (MR mode); writes policy violation to logs (direct mode).\n async handlePromote() {\n const st = s();\n if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }\n if (st.promoteMode === 'direct' && !String(st.promoteReason || '').trim()) { st.toast('Для небезопасного прямого переноса укажите причину', 'error'); return; }\n u({ promoting: true });\n try {\n const response = await gitService.promote(st.dashboardId, {\n from_branch: st.promoteFromBranch, to_branch: st.promoteToBranch, mode: st.promoteMode,\n title: `Promote ${st.promoteFromBranch} -> ${st.promoteToBranch}: ${st.dashboardTitle || st.dashboardId}`,\n description: st.promoteMode === 'direct' ? `Unsafe direct promote.\\nReason: ${st.promoteReason}` : undefined,\n reason: st.promoteMode === 'direct' ? st.promoteReason : undefined,\n }, st.envId);\n if (st.promoteMode === 'direct') { st.toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning'); }\n else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }\n } catch (e) { st.toast(e.message, 'error'); } finally { u({ promoting: false }); }\n },\n // #endregion handlePromote\n\n // #region openDeployModal [C:2] [TYPE Function]\n // @BRIEF Open deployment target modal with PROD confirmation.\n // @PRE dashboardId is a valid slug.\n // @POST showDeployModal set to true if PROD confirmation passes.\n openDeployModal() {\n const st = s();\n if (st.currentEnvStage === 'PROD') {\n const expected = String(st.dashboardId);\n const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);\n if (String(confirmation || '').trim() !== expected) { st.toast('Подтверждение PROD не пройдено. Деплой отменен.', 'error'); return; }\n }\n u({ showDeployModal: true });\n },\n // #endregion openDeployModal\n\n // #region handleCreateRemoteRepo [C:2] [TYPE Function]\n // @BRIEF Create a remote repository on the selected Git provider.\n // @PRE A Git config is selected; user provides a repo name via prompt.\n // @POST remoteUrl set to created (or existing) repository clone URL.\n // @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.\n async handleCreateRemoteRepo() {\n const config = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }\n if (!s().selectedConfigId && config.id) u({ selectedConfigId: config.id });\n const suggestedName = buildSuggestedRepoName(s().dashboardTitle, s().dashboardId);\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n u({ creatingRemoteRepo: true });\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName, private: true,\n description: `Superset dashboard ${s().dashboardId}: ${s().dashboardTitle || repoName}`,\n auto_init: true, default_branch: 'main',\n });\n const url = repo?.clone_url || repo?.html_url || '';\n if (!url) throw new Error('Remote repository created, but URL is empty');\n u({ remoteUrl: url });\n s().toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) {\n if (e?.status === 409 && /already exists/i.test(String(e?.message || ''))) {\n s().toast(s().t?.git?.repo_already_exists || 'Repository already exists. Enter its URL below and click Init.', 'warning');\n } else { s().toast(e.message, 'error'); }\n } finally { u({ creatingRemoteRepo: false }); }\n },\n // #endregion handleCreateRemoteRepo\n\n // #region handleInit [C:3] [TYPE Function]\n // @BRIEF Initialize local Git repository for the dashboard: clone remote.\n // @PRE selectedConfigId is set; remoteUrl is set; envId required when ref is slug.\n // @POST Repository initialized; initialized=true; workspace loaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/init; loads workspace on success.\n async handleInit() {\n if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }\n // ── Guard: slug-based init requires envId ──\n if (!s().envId && !isNumericDashboardRef(s().dashboardId)) {\n s().toast('Environment must be selected to initialize Git for this dashboard.', 'error');\n return;\n }\n u({ loading: true });\n try {\n await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);\n s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');\n const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';\n u({ initialized: true, repositoryProvider: provider });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n // #endregion handleInit\n };\n}\n// #endregion UseGitManager\n" + "body": "// #region UseGitManager [C:3] [TYPE Module] [SEMANTICS git, handlers, composable, orchestration]\n// @BRIEF Composable for GitManager handler functions — extracted to reduce component size per INV_7.\n// @RELATION DEPENDS_ON -> [EXT:frontend:gitService]\n// @RELATION DEPENDS_ON -> [GitUtils]\n// @RATIONALE Extracted handler logic from GitManager (1220→~350 lines) to meet INV_7 <150 line contract limit.\nimport { gitService } from '../../services/gitService';\nimport { api } from '../../lib/api.js';\nimport { isNumericDashboardRef, resolveDefaultConfig, extractUnfinishedMergeContext, buildSuggestedRepoName } from '../../services/git-utils.js';\n\n// #region buildGitErrorFromException [C:2] [TYPE Function]\n// @BRIEF Extract structured gitError payload from a caught exception for modal error banner.\n// @PRE e is an Error object with optional .status, .detail, .error_code properties.\n// @POST Returns { message, status, error_code, files, next_steps, detail } for state.\nfunction buildGitErrorFromException(e) {\n if (!e) return { message: 'Неизвестная ошибка Git', status: 500, error_code: null, files: [], next_steps: [], detail: null };\n const detail = e?.detail || (e?.detail === 'string' ? { message: e.detail } : null) || null;\n const detailObj = typeof detail === 'object' ? detail : null;\n return {\n message: detailObj?.message || detailObj?.message_en || e.message || 'Произошла ошибка Git',\n status: e?.status || 500,\n error_code: e?.error_code || detailObj?.error_code || null,\n files: detailObj?.files || [],\n next_steps: detailObj?.next_steps || detailObj?.next_steps_en || [],\n detail: detailObj,\n };\n}\n// #endregion buildGitErrorFromException\n\nexport function createGitHandlers(getState, setState) {\n const s = () => getState();\n const u = (patch) => setState(patch);\n\n // ── Helper: clear modal error banner ──\n function clearError() {\n u({ gitError: null, gitErrorType: 'error' });\n }\n\n // ── Helper: set modal error banner + persistent toast ──\n function setGitError(e, errorType) {\n const err = buildGitErrorFromException(e);\n err.errorType = errorType || (err.status >= 500 ? 'error' : err.status === 409 ? 'warning' : 'error');\n u({ gitError: err, gitErrorType: err.errorType });\n // Show persistent toast above the modal (duration=0 = no auto-dismiss)\n if (s().toast) {\n const type = err.errorType === 'warning' ? 'warning' : 'error';\n s().toast(err.message, type, 0);\n }\n }\n\n // ── Use const reference instead of `this` to avoid context loss in callbacks ──\n const handlers = {\n\n // #region loadCurrentEnvironmentStage [C:2] [TYPE Function]\n // @BRIEF Resolve current environment stage (DEV/PREPROD/PROD) and apply GitFlow defaults.\n // @PRE resolveEnvId() returns a valid environment ID, or handler exits silently.\n // @POST currentEnvStage and GitFlow defaults (promoteFromBranch, promoteToBranch) are set.\n // @SIDE_EFFECT Fetches /environments from API.\n async loadCurrentEnvironmentStage() {\n const currentEnvId = s().resolveEnvId();\n if (!currentEnvId) return;\n try {\n const environments = await api.getEnvironmentsList();\n const currentEnv = (environments || []).find((item) => item.id === currentEnvId);\n if (!currentEnv) return;\n const stage = s().normalizeEnvStageFn(currentEnv);\n u({ currentEnvStage: stage });\n const defaults = s().applyGitflowStageDefaultsFn(stage);\n u(defaults);\n } catch (e) {\n console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);\n }\n },\n // #endregion loadCurrentEnvironmentStage\n\n // #region checkStatus [C:3] [TYPE Function]\n // @BRIEF Check if Git repository is initialized for the dashboard.\n // @PRE dashboardId is a non-numeric slug; envId is required (resolved with localStorage fallback).\n // @POST initialized=true if repository found and workspace loaded; initialized=false if not found or envId missing.\n // @SIDE_EFFECT Fetches /git/repositories/{ref}/branches; may fetch /git/repositories/{ref}/status and /diff on success.\n async checkStatus() {\n if (isNumericDashboardRef(s().dashboardId)) {\n u({ checkingStatus: false, initialized: false });\n s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n u({ checkingStatus: true });\n if (!s().envId) {\n u({ initialized: false, checkingStatus: false });\n return;\n }\n try {\n await gitService.getBranches(s().dashboardId, s().envId);\n u({ initialized: true });\n await handlers.loadWorkspace();\n } catch (_e) {\n u({ initialized: false });\n } finally { u({ checkingStatus: false }); }\n },\n // #endregion checkStatus\n\n // #region loadWorkspace [C:2] [TYPE Function]\n // @BRIEF Load Git workspace status and diff for initialized repository.\n // @PRE initialized is true; envId is required when ref is slug.\n // @POST workspaceStatus and workspaceDiff are updated; currentBranch synced from server.\n // @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.\n async loadWorkspace() {\n if (!s().initialized) return;\n clearError();\n u({ workspaceLoading: true });\n try {\n const ws = await gitService.getStatus(s().dashboardId, s().envId);\n const sd = await gitService.getDiff(s().dashboardId, null, true, s().envId);\n const ud = await gitService.getDiff(s().dashboardId, null, false, s().envId);\n u({ workspaceStatus: ws, workspaceDiff: [sd, ud].filter(Boolean).join('\\n\\n'), currentBranch: ws?.current_branch || s().currentBranch });\n } catch (e) {\n setGitError(e);\n } finally { u({ workspaceLoading: false }); }\n },\n // #endregion loadWorkspace\n\n // #region handleSync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @PRE dashboardId is a non-numeric slug; envId required when ref is slug.\n // @POST Dashboard state synced to Git workspace; workspace reloaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync; reloads workspace on success.\n async handleSync() {\n if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }\n clearError();\n u({ loading: true });\n try {\n const sourceEnvId = s().envId || localStorage.getItem('selected_env_id');\n await gitService.sync(s().dashboardId, sourceEnvId, s().envId);\n s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ loading: false }); }\n },\n // #endregion handleSync\n\n // #region handleGenerateMessage [C:2] [TYPE Function]\n // @BRIEF Generate AI commit message from dashboard diff.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.\n // @POST commitMessage state updated with generated text.\n async handleGenerateMessage() {\n clearError();\n u({ generatingMessage: true });\n try {\n const data = await api.postApi(`/git/repositories/${encodeURIComponent(String(s().dashboardId))}/generate-message${s().envId ? `?env_id=${encodeURIComponent(String(s().envId))}` : ''}`, undefined, { suppressToast: true });\n u({ commitMessage: data?.message || '' });\n s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');\n } catch (e) {\n setGitError(e);\n } finally { u({ generatingMessage: false }); }\n },\n // #endregion handleGenerateMessage\n\n // #region handleCommit [C:2] [TYPE Function]\n // @BRIEF Stage and commit workspace changes, optionally push.\n // @PRE commitMessage is non-empty; hasWorkspaceChanges is true; envId required when ref is slug.\n // @POST Changes committed (and pushed if autoPushAfterCommit); workspace reloaded.\n // @SIDE_EFFECT POSTs to /commit and optionally /push.\n async handleCommit() {\n if (!s().commitMessage || !s().hasWorkspaceChanges) return;\n clearError();\n u({ committing: true });\n try {\n await gitService.commit(s().dashboardId, s().commitMessage, [], s().envId);\n if (s().autoPushAfterCommit) {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');\n } else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }\n u({ commitMessage: '' });\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ committing: false }); }\n },\n // #endregion handleCommit\n\n // #region openUnfinishedMergeDialogFromError [C:2] [TYPE Function]\n // @BRIEF Parse error for unfinished merge context and open dialog.\n // @PRE error contains GIT_UNFINISHED_MERGE error_code or related detail.\n // @POST Returns true and opens dialog if merge context found; false otherwise.\n openUnfinishedMergeDialogFromError(error) {\n const context = extractUnfinishedMergeContext(error);\n if (!context) return false;\n u({ unfinishedMergeContext: context, showUnfinishedMergeDialog: true });\n return true;\n },\n // #endregion openUnfinishedMergeDialogFromError\n\n // #region loadMergeRecoveryState [C:2] [TYPE Function]\n // @BRIEF Load merge recovery state from backend for unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST unfinishedMergeContext is populated; dialog closed if no merge in progress.\n // @SIDE_EFFECT GETs /merge/status from backend.\n async loadMergeRecoveryState() {\n clearError();\n u({ mergeRecoveryLoading: true });\n try {\n const status = await gitService.getMergeStatus(s().dashboardId, s().envId);\n if (!status?.has_unfinished_merge) { handlers.closeUnfinishedMergeDialog(); return; }\n u({\n unfinishedMergeContext: {\n ...(s().unfinishedMergeContext || {}),\n message: s().unfinishedMergeContext?.message || (s().t?.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || s().unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || s().unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || s().unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || s().unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || ''),\n nextSteps: Array.isArray(s().unfinishedMergeContext?.nextSteps) ? s().unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(s().unfinishedMergeContext?.commands) ? s().unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n }\n });\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion loadMergeRecoveryState\n\n // #region closeUnfinishedMergeDialog [C:1] [TYPE Function]\n // @BRIEF Close unfinished merge dialog and reset merge state.\n // @POST Dialog hidden; mergeConflicts, showConflictResolver reset.\n closeUnfinishedMergeDialog() {\n u({ showUnfinishedMergeDialog: false, unfinishedMergeContext: null, mergeConflicts: [], showConflictResolver: false });\n },\n // #endregion closeUnfinishedMergeDialog\n\n // #region handleOpenConflictResolver [C:2] [TYPE Function]\n // @BRIEF Load merge conflicts and open conflict resolver UI.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST mergeConflicts populated; showConflictResolver set to true if conflicts found.\n // @SIDE_EFFECT GETs /merge/conflicts from backend.\n async handleOpenConflictResolver() {\n clearError();\n u({ mergeRecoveryLoading: true });\n try {\n const mc = await gitService.getMergeConflicts(s().dashboardId, s().envId);\n if (!Array.isArray(mc) || mc.length === 0) { s().toast(s().t?.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info'); return; }\n u({ mergeConflicts: mc, showConflictResolver: true });\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion handleOpenConflictResolver\n\n // #region handleResolveConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolutions and stage resolved files.\n // @PRE event.detail maps file paths to resolution strategies; envId required when ref is slug.\n // @POST Conflicts resolved and staged; resolver closed; workspace reloaded.\n async handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));\n if (!resolutions.length) { s().toast(s().t?.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning'); return; }\n clearError();\n u({ mergeResolveInProgress: true });\n try {\n await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n u({ showConflictResolver: false });\n await handlers.loadMergeRecoveryState();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeResolveInProgress: false }); }\n },\n // #endregion handleResolveConflicts\n\n // #region handleAbortUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE envId required when ref is slug.\n // @POST Merge aborted; dialog closed; workspace reloaded.\n async handleAbortUnfinishedMerge() {\n clearError();\n u({ mergeAbortInProgress: true });\n try {\n await gitService.abortMerge(s().dashboardId, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n handlers.closeUnfinishedMergeDialog();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeAbortInProgress: false }); }\n },\n // #endregion handleAbortUnfinishedMerge\n\n // #region handleContinueUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating merge commit.\n // @PRE All conflicts resolved; envId required when ref is slug.\n // @POST Merge commit created; dialog closed; workspace reloaded.\n async handleContinueUnfinishedMerge() {\n clearError();\n u({ mergeContinueInProgress: true });\n try {\n await gitService.continueMerge(s().dashboardId, '', s().envId);\n s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n handlers.closeUnfinishedMergeDialog();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n await handlers.loadMergeRecoveryState();\n } finally { u({ mergeContinueInProgress: false }); }\n },\n // #endregion handleContinueUnfinishedMerge\n\n // #region getUnfinishedMergeCommandsText [C:1] [TYPE Function]\n // @BRIEF Get shell commands for manual merge recovery.\n // @POST Returns concatenated command string or empty string.\n getUnfinishedMergeCommandsText() {\n if (!s().unfinishedMergeContext?.commands?.length) return '';\n return s().unfinishedMergeContext.commands.join('\\n');\n },\n // #endregion getUnfinishedMergeCommandsText\n\n // #region handleCopyUnfinishedMergeCommands [C:1] [TYPE Function]\n // @BRIEF Copy merge recovery commands to clipboard.\n // @PRE commands exist in unfinishedMergeContext.\n // @POST Commands copied to clipboard; toast shown.\n async handleCopyUnfinishedMergeCommands() {\n const text = handlers.getUnfinishedMergeCommandsText();\n if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }\n u({ copyingUnfinishedMergeCommands: true });\n try {\n await navigator.clipboard.writeText(text);\n s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }\n },\n // #endregion handleCopyUnfinishedMergeCommands\n\n // #region handlePull [C:2] [TYPE Function]\n // @BRIEF Pull changes from remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Local repository updated with remote changes; workspace reloaded.\n // @SIDE_EFFECT POSTs to /pull; may open merge dialog on GIT_UNFINISHED_MERGE.\n async handlePull() {\n clearError();\n u({ isPulling: true });\n try {\n await gitService.pull(s().dashboardId, s().envId);\n s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n const handled = handlers.openUnfinishedMergeDialogFromError(e);\n if (handled) {\n await handlers.loadMergeRecoveryState();\n } else {\n setGitError(e);\n }\n } finally { u({ isPulling: false }); }\n },\n // #endregion handlePull\n\n // #region handlePush [C:2] [TYPE Function]\n // @BRIEF Push local commits to remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Remote updated with local commits; workspace reloaded.\n async handlePush() {\n clearError();\n u({ isPushing: true });\n try {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ isPushing: false }); }\n },\n // #endregion handlePush\n\n // #region handlePromote [C:3] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE promoteFromBranch and promoteToBranch differ; direct mode requires reason; envId required when ref is slug.\n // @POST MR created on Git server (MR mode) or direct merge executed (direct mode).\n // @SIDE_EFFECT Opens MR URL in new tab (MR mode); writes policy violation to logs (direct mode).\n async handlePromote() {\n const st = s();\n if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }\n if (st.promoteMode === 'direct' && !String(st.promoteReason || '').trim()) { st.toast('Для небезопасного прямого переноса укажите причину', 'error'); return; }\n clearError();\n u({ promoting: true });\n try {\n const response = await gitService.promote(st.dashboardId, {\n from_branch: st.promoteFromBranch, to_branch: st.promoteToBranch, mode: st.promoteMode,\n title: `Promote ${st.promoteFromBranch} -> ${st.promoteToBranch}: ${st.dashboardTitle || st.dashboardId}`,\n description: st.promoteMode === 'direct' ? `Unsafe direct promote.\\nReason: ${st.promoteReason}` : undefined,\n reason: st.promoteMode === 'direct' ? st.promoteReason : undefined,\n }, st.envId);\n if (st.promoteMode === 'direct') { st.toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning'); }\n else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }\n } catch (e) {\n setGitError(e);\n } finally { u({ promoting: false }); }\n },\n // #endregion handlePromote\n\n // #region openDeployModal [C:2] [TYPE Function]\n // @BRIEF Open deployment target modal with PROD confirmation.\n // @PRE dashboardId is a valid slug.\n // @POST showDeployModal set to true if PROD confirmation passes.\n openDeployModal() {\n const st = s();\n if (st.currentEnvStage === 'PROD') {\n const expected = String(st.dashboardId);\n const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);\n if (String(confirmation || '').trim() !== expected) { st.toast('Подтверждение PROD не пройдено. Деплой отменен.', 'error'); return; }\n }\n u({ showDeployModal: true });\n },\n // #endregion openDeployModal\n\n // #region handleCreateRemoteRepo [C:2] [TYPE Function]\n // @BRIEF Create a remote repository on the selected Git provider.\n // @PRE A Git config is selected; user provides a repo name via prompt.\n // @POST remoteUrl set to created (or existing) repository clone URL.\n // @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.\n async handleCreateRemoteRepo() {\n const config = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }\n if (!s().selectedConfigId && config.id) u({ selectedConfigId: config.id });\n const suggestedName = buildSuggestedRepoName(s().dashboardTitle, s().dashboardId);\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n clearError();\n u({ creatingRemoteRepo: true });\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName, private: true,\n description: `Superset dashboard ${s().dashboardId}: ${s().dashboardTitle || repoName}`,\n auto_init: true, default_branch: 'main',\n });\n const url = repo?.clone_url || repo?.html_url || '';\n if (!url) throw new Error('Remote repository created, but URL is empty');\n u({ remoteUrl: url });\n s().toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) {\n if (e?.status === 409 && /already exists/i.test(String(e?.message || ''))) {\n s().toast(s().t?.git?.repo_already_exists || 'Repository already exists. Enter its URL below and click Init.', 'warning', 0);\n } else {\n setGitError(e, 'warning');\n }\n } finally { u({ creatingRemoteRepo: false }); }\n },\n // #endregion handleCreateRemoteRepo\n\n // #region handleInit [C:3] [TYPE Function]\n // @BRIEF Initialize local Git repository for the dashboard: clone remote.\n // @PRE selectedConfigId is set; remoteUrl is set; envId required when ref is slug.\n // @POST Repository initialized; initialized=true; workspace loaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/init; loads workspace on success.\n async handleInit() {\n if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }\n // ── Guard: slug-based init requires envId ──\n if (!s().envId && !isNumericDashboardRef(s().dashboardId)) {\n s().toast('Environment must be selected to initialize Git for this dashboard.', 'error');\n return;\n }\n clearError();\n u({ loading: true });\n try {\n await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);\n s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');\n const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';\n u({ initialized: true, repositoryProvider: provider });\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ loading: false }); }\n },\n // #endregion handleInit\n };\n return handlers;\n}\n// #endregion UseGitManager\n" + }, + { + "contract_id": "buildGitErrorFromException", + "contract_type": "Function", + "file_path": "frontend/src/components/git/useGitManager.js", + "start_line": 10, + "end_line": 27, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Extract structured gitError payload from a caught exception for modal error banner.", + "COMPLEXITY": 2, + "POST": "Returns { message, status, error_code, files, next_steps, detail } for state.", + "PRE": "e is an Error object with optional .status, .detail, .error_code properties." + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region buildGitErrorFromException [C:2] [TYPE Function]\n// @BRIEF Extract structured gitError payload from a caught exception for modal error banner.\n// @PRE e is an Error object with optional .status, .detail, .error_code properties.\n// @POST Returns { message, status, error_code, files, next_steps, detail } for state.\nfunction buildGitErrorFromException(e) {\n if (!e) return { message: 'Неизвестная ошибка Git', status: 500, error_code: null, files: [], next_steps: [], detail: null };\n const detail = e?.detail || (e?.detail === 'string' ? { message: e.detail } : null) || null;\n const detailObj = typeof detail === 'object' ? detail : null;\n return {\n message: detailObj?.message || detailObj?.message_en || e.message || 'Произошла ошибка Git',\n status: e?.status || 500,\n error_code: e?.error_code || detailObj?.error_code || null,\n files: detailObj?.files || [],\n next_steps: detailObj?.next_steps || detailObj?.next_steps_en || [],\n detail: detailObj,\n };\n}\n// #endregion buildGitErrorFromException\n" }, { "contract_id": "loadCurrentEnvironmentStage", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 16, - "end_line": 36, + "start_line": 53, + "end_line": 73, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61640,8 +62649,8 @@ "contract_id": "checkStatus", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 38, - "end_line": 62, + "start_line": 75, + "end_line": 99, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -61655,14 +62664,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region checkStatus [C:3] [TYPE Function]\n // @BRIEF Check if Git repository is initialized for the dashboard.\n // @PRE dashboardId is a non-numeric slug; envId is required (resolved with localStorage fallback).\n // @POST initialized=true if repository found and workspace loaded; initialized=false if not found or envId missing.\n // @SIDE_EFFECT Fetches /git/repositories/{ref}/branches; may fetch /git/repositories/{ref}/status and /diff on success.\n async checkStatus() {\n if (isNumericDashboardRef(s().dashboardId)) {\n u({ checkingStatus: false, initialized: false });\n s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n u({ checkingStatus: true });\n if (!s().envId) {\n u({ initialized: false, checkingStatus: false });\n return;\n }\n try {\n await gitService.getBranches(s().dashboardId, s().envId);\n u({ initialized: true });\n await this.loadWorkspace();\n } catch (_e) {\n u({ initialized: false });\n } finally { u({ checkingStatus: false }); }\n },\n // #endregion checkStatus\n" + "body": " // #region checkStatus [C:3] [TYPE Function]\n // @BRIEF Check if Git repository is initialized for the dashboard.\n // @PRE dashboardId is a non-numeric slug; envId is required (resolved with localStorage fallback).\n // @POST initialized=true if repository found and workspace loaded; initialized=false if not found or envId missing.\n // @SIDE_EFFECT Fetches /git/repositories/{ref}/branches; may fetch /git/repositories/{ref}/status and /diff on success.\n async checkStatus() {\n if (isNumericDashboardRef(s().dashboardId)) {\n u({ checkingStatus: false, initialized: false });\n s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');\n return;\n }\n u({ checkingStatus: true });\n if (!s().envId) {\n u({ initialized: false, checkingStatus: false });\n return;\n }\n try {\n await gitService.getBranches(s().dashboardId, s().envId);\n u({ initialized: true });\n await handlers.loadWorkspace();\n } catch (_e) {\n u({ initialized: false });\n } finally { u({ checkingStatus: false }); }\n },\n // #endregion checkStatus\n" }, { "contract_id": "loadWorkspace", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 64, - "end_line": 81, + "start_line": 101, + "end_line": 119, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61676,14 +62685,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region loadWorkspace [C:2] [TYPE Function]\n // @BRIEF Load Git workspace status and diff for initialized repository.\n // @PRE initialized is true; envId is required when ref is slug.\n // @POST workspaceStatus and workspaceDiff are updated; currentBranch synced from server.\n // @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.\n async loadWorkspace() {\n if (!s().initialized) return;\n u({ workspaceLoading: true });\n try {\n const ws = await gitService.getStatus(s().dashboardId, s().envId);\n const sd = await gitService.getDiff(s().dashboardId, null, true, s().envId);\n const ud = await gitService.getDiff(s().dashboardId, null, false, s().envId);\n u({ workspaceStatus: ws, workspaceDiff: [sd, ud].filter(Boolean).join('\\n\\n'), currentBranch: ws?.current_branch || s().currentBranch });\n } catch (e) {\n s().toast(e.message || 'Не удалось загрузить изменения', 'error');\n } finally { u({ workspaceLoading: false }); }\n },\n // #endregion loadWorkspace\n" + "body": " // #region loadWorkspace [C:2] [TYPE Function]\n // @BRIEF Load Git workspace status and diff for initialized repository.\n // @PRE initialized is true; envId is required when ref is slug.\n // @POST workspaceStatus and workspaceDiff are updated; currentBranch synced from server.\n // @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.\n async loadWorkspace() {\n if (!s().initialized) return;\n clearError();\n u({ workspaceLoading: true });\n try {\n const ws = await gitService.getStatus(s().dashboardId, s().envId);\n const sd = await gitService.getDiff(s().dashboardId, null, true, s().envId);\n const ud = await gitService.getDiff(s().dashboardId, null, false, s().envId);\n u({ workspaceStatus: ws, workspaceDiff: [sd, ud].filter(Boolean).join('\\n\\n'), currentBranch: ws?.current_branch || s().currentBranch });\n } catch (e) {\n setGitError(e);\n } finally { u({ workspaceLoading: false }); }\n },\n // #endregion loadWorkspace\n" }, { "contract_id": "handleSync", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 83, - "end_line": 98, + "start_line": 121, + "end_line": 139, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61697,14 +62706,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleSync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @PRE dashboardId is a non-numeric slug; envId required when ref is slug.\n // @POST Dashboard state synced to Git workspace; workspace reloaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync; reloads workspace on success.\n async handleSync() {\n if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }\n u({ loading: true });\n try {\n const sourceEnvId = localStorage.getItem('selected_env_id');\n await gitService.sync(s().dashboardId, sourceEnvId, s().envId);\n s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n // #endregion handleSync\n" + "body": " // #region handleSync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @PRE dashboardId is a non-numeric slug; envId required when ref is slug.\n // @POST Dashboard state synced to Git workspace; workspace reloaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync; reloads workspace on success.\n async handleSync() {\n if (isNumericDashboardRef(s().dashboardId)) { s().toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error'); return; }\n clearError();\n u({ loading: true });\n try {\n const sourceEnvId = s().envId || localStorage.getItem('selected_env_id');\n await gitService.sync(s().dashboardId, sourceEnvId, s().envId);\n s().toast(s().t?.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ loading: false }); }\n },\n // #endregion handleSync\n" }, { "contract_id": "handleGenerateMessage", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 100, - "end_line": 112, + "start_line": 141, + "end_line": 156, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61717,14 +62726,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleGenerateMessage [C:2] [TYPE Function]\n // @BRIEF Generate AI commit message from dashboard diff.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.\n // @POST commitMessage state updated with generated text.\n async handleGenerateMessage() {\n u({ generatingMessage: true });\n try {\n const data = await api.postApi(`/git/repositories/${encodeURIComponent(String(s().dashboardId))}/generate-message${s().envId ? `?env_id=${encodeURIComponent(String(s().envId))}` : ''}`, undefined, { suppressToast: true });\n u({ commitMessage: data?.message || '' });\n s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');\n } catch (e) { s().toast(e.message || s().t?.git?.commit_message_failed || 'Не удалось сгенерировать сообщение', 'error'); } finally { u({ generatingMessage: false }); }\n },\n // #endregion handleGenerateMessage\n" + "body": " // #region handleGenerateMessage [C:2] [TYPE Function]\n // @BRIEF Generate AI commit message from dashboard diff.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.\n // @POST commitMessage state updated with generated text.\n async handleGenerateMessage() {\n clearError();\n u({ generatingMessage: true });\n try {\n const data = await api.postApi(`/git/repositories/${encodeURIComponent(String(s().dashboardId))}/generate-message${s().envId ? `?env_id=${encodeURIComponent(String(s().envId))}` : ''}`, undefined, { suppressToast: true });\n u({ commitMessage: data?.message || '' });\n s().toast(s().t?.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');\n } catch (e) {\n setGitError(e);\n } finally { u({ generatingMessage: false }); }\n },\n // #endregion handleGenerateMessage\n" }, { "contract_id": "handleCommit", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 114, - "end_line": 132, + "start_line": 158, + "end_line": 179, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61738,14 +62747,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleCommit [C:2] [TYPE Function]\n // @BRIEF Stage and commit workspace changes, optionally push.\n // @PRE commitMessage is non-empty; hasWorkspaceChanges is true; envId required when ref is slug.\n // @POST Changes committed (and pushed if autoPushAfterCommit); workspace reloaded.\n // @SIDE_EFFECT POSTs to /commit and optionally /push.\n async handleCommit() {\n if (!s().commitMessage || !s().hasWorkspaceChanges) return;\n u({ committing: true });\n try {\n await gitService.commit(s().dashboardId, s().commitMessage, [], s().envId);\n if (s().autoPushAfterCommit) {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');\n } else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }\n u({ commitMessage: '' });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ committing: false }); }\n },\n // #endregion handleCommit\n" + "body": " // #region handleCommit [C:2] [TYPE Function]\n // @BRIEF Stage and commit workspace changes, optionally push.\n // @PRE commitMessage is non-empty; hasWorkspaceChanges is true; envId required when ref is slug.\n // @POST Changes committed (and pushed if autoPushAfterCommit); workspace reloaded.\n // @SIDE_EFFECT POSTs to /commit and optionally /push.\n async handleCommit() {\n if (!s().commitMessage || !s().hasWorkspaceChanges) return;\n clearError();\n u({ committing: true });\n try {\n await gitService.commit(s().dashboardId, s().commitMessage, [], s().envId);\n if (s().autoPushAfterCommit) {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');\n } else { s().toast(s().t?.git?.commit_success || 'Коммит успешно создан', 'success'); }\n u({ commitMessage: '' });\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ committing: false }); }\n },\n // #endregion handleCommit\n" }, { "contract_id": "openUnfinishedMergeDialogFromError", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 134, - "end_line": 144, + "start_line": 181, + "end_line": 191, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61764,8 +62773,8 @@ "contract_id": "loadMergeRecoveryState", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 146, - "end_line": 172, + "start_line": 193, + "end_line": 222, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61779,14 +62788,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region loadMergeRecoveryState [C:2] [TYPE Function]\n // @BRIEF Load merge recovery state from backend for unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST unfinishedMergeContext is populated; dialog closed if no merge in progress.\n // @SIDE_EFFECT GETs /merge/status from backend.\n async loadMergeRecoveryState() {\n u({ mergeRecoveryLoading: true });\n try {\n const status = await gitService.getMergeStatus(s().dashboardId, s().envId);\n if (!status?.has_unfinished_merge) { this.closeUnfinishedMergeDialog(); return; }\n u({\n unfinishedMergeContext: {\n ...(s().unfinishedMergeContext || {}),\n message: s().unfinishedMergeContext?.message || (s().t?.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || s().unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || s().unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || s().unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || s().unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || ''),\n nextSteps: Array.isArray(s().unfinishedMergeContext?.nextSteps) ? s().unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(s().unfinishedMergeContext?.commands) ? s().unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n }\n });\n } catch (e) { s().toast(e.message || 'Failed to load merge status', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion loadMergeRecoveryState\n" + "body": " // #region loadMergeRecoveryState [C:2] [TYPE Function]\n // @BRIEF Load merge recovery state from backend for unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST unfinishedMergeContext is populated; dialog closed if no merge in progress.\n // @SIDE_EFFECT GETs /merge/status from backend.\n async loadMergeRecoveryState() {\n clearError();\n u({ mergeRecoveryLoading: true });\n try {\n const status = await gitService.getMergeStatus(s().dashboardId, s().envId);\n if (!status?.has_unfinished_merge) { handlers.closeUnfinishedMergeDialog(); return; }\n u({\n unfinishedMergeContext: {\n ...(s().unfinishedMergeContext || {}),\n message: s().unfinishedMergeContext?.message || (s().t?.git?.unfinished_merge?.default_message || ''),\n repositoryPath: String(status.repository_path || s().unfinishedMergeContext?.repositoryPath || ''),\n gitDir: String(status.git_dir || s().unfinishedMergeContext?.gitDir || ''),\n currentBranch: String(status.current_branch || s().unfinishedMergeContext?.currentBranch || ''),\n mergeHead: String(status.merge_head || s().unfinishedMergeContext?.mergeHead || ''),\n mergeMessagePreview: String(status.merge_message_preview || ''),\n nextSteps: Array.isArray(s().unfinishedMergeContext?.nextSteps) ? s().unfinishedMergeContext.nextSteps : [],\n commands: Array.isArray(s().unfinishedMergeContext?.commands) ? s().unfinishedMergeContext.commands : [],\n conflictsCount: Number(status.conflicts_count || 0),\n }\n });\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion loadMergeRecoveryState\n" }, { "contract_id": "closeUnfinishedMergeDialog", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 174, - "end_line": 180, + "start_line": 224, + "end_line": 230, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -61804,8 +62813,8 @@ "contract_id": "handleOpenConflictResolver", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 182, - "end_line": 195, + "start_line": 232, + "end_line": 248, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61819,14 +62828,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleOpenConflictResolver [C:2] [TYPE Function]\n // @BRIEF Load merge conflicts and open conflict resolver UI.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST mergeConflicts populated; showConflictResolver set to true if conflicts found.\n // @SIDE_EFFECT GETs /merge/conflicts from backend.\n async handleOpenConflictResolver() {\n u({ mergeRecoveryLoading: true });\n try {\n const mc = await gitService.getMergeConflicts(s().dashboardId, s().envId);\n if (!Array.isArray(mc) || mc.length === 0) { s().toast(s().t?.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info'); return; }\n u({ mergeConflicts: mc, showConflictResolver: true });\n } catch (e) { s().toast(e.message || 'Failed to load merge conflicts', 'error'); } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion handleOpenConflictResolver\n" + "body": " // #region handleOpenConflictResolver [C:2] [TYPE Function]\n // @BRIEF Load merge conflicts and open conflict resolver UI.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST mergeConflicts populated; showConflictResolver set to true if conflicts found.\n // @SIDE_EFFECT GETs /merge/conflicts from backend.\n async handleOpenConflictResolver() {\n clearError();\n u({ mergeRecoveryLoading: true });\n try {\n const mc = await gitService.getMergeConflicts(s().dashboardId, s().envId);\n if (!Array.isArray(mc) || mc.length === 0) { s().toast(s().t?.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info'); return; }\n u({ mergeConflicts: mc, showConflictResolver: true });\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeRecoveryLoading: false }); }\n },\n // #endregion handleOpenConflictResolver\n" }, { "contract_id": "handleResolveConflicts", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 197, - "end_line": 214, + "start_line": 250, + "end_line": 270, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61839,14 +62848,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleResolveConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolutions and stage resolved files.\n // @PRE event.detail maps file paths to resolution strategies; envId required when ref is slug.\n // @POST Conflicts resolved and staged; resolver closed; workspace reloaded.\n async handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));\n if (!resolutions.length) { s().toast(s().t?.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning'); return; }\n u({ mergeResolveInProgress: true });\n try {\n await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n u({ showConflictResolver: false });\n await this.loadMergeRecoveryState();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to resolve conflicts', 'error'); } finally { u({ mergeResolveInProgress: false }); }\n },\n // #endregion handleResolveConflicts\n" + "body": " // #region handleResolveConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolutions and stage resolved files.\n // @PRE event.detail maps file paths to resolution strategies; envId required when ref is slug.\n // @POST Conflicts resolved and staged; resolver closed; workspace reloaded.\n async handleResolveConflicts(event) {\n const detail = event?.detail || {};\n const resolutions = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));\n if (!resolutions.length) { s().toast(s().t?.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning'); return; }\n clearError();\n u({ mergeResolveInProgress: true });\n try {\n await gitService.resolveMergeConflicts(s().dashboardId, resolutions, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');\n u({ showConflictResolver: false });\n await handlers.loadMergeRecoveryState();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeResolveInProgress: false }); }\n },\n // #endregion handleResolveConflicts\n" }, { "contract_id": "handleAbortUnfinishedMerge", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 216, - "end_line": 229, + "start_line": 272, + "end_line": 288, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61859,14 +62868,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleAbortUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE envId required when ref is slug.\n // @POST Merge aborted; dialog closed; workspace reloaded.\n async handleAbortUnfinishedMerge() {\n u({ mergeAbortInProgress: true });\n try {\n await gitService.abortMerge(s().dashboardId, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message || 'Failed to abort merge', 'error'); } finally { u({ mergeAbortInProgress: false }); }\n },\n // #endregion handleAbortUnfinishedMerge\n" + "body": " // #region handleAbortUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE envId required when ref is slug.\n // @POST Merge aborted; dialog closed; workspace reloaded.\n async handleAbortUnfinishedMerge() {\n clearError();\n u({ mergeAbortInProgress: true });\n try {\n await gitService.abortMerge(s().dashboardId, s().envId);\n s().toast(s().t?.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');\n handlers.closeUnfinishedMergeDialog();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ mergeAbortInProgress: false }); }\n },\n // #endregion handleAbortUnfinishedMerge\n" }, { "contract_id": "handleContinueUnfinishedMerge", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 231, - "end_line": 247, + "start_line": 290, + "end_line": 307, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61879,14 +62888,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleContinueUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating merge commit.\n // @PRE All conflicts resolved; envId required when ref is slug.\n // @POST Merge commit created; dialog closed; workspace reloaded.\n async handleContinueUnfinishedMerge() {\n u({ mergeContinueInProgress: true });\n try {\n await gitService.continueMerge(s().dashboardId, '', s().envId);\n s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n this.closeUnfinishedMergeDialog();\n await this.loadWorkspace();\n } catch (e) {\n s().toast(e.message || 'Failed to continue merge', 'error');\n await this.loadMergeRecoveryState();\n } finally { u({ mergeContinueInProgress: false }); }\n },\n // #endregion handleContinueUnfinishedMerge\n" + "body": " // #region handleContinueUnfinishedMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating merge commit.\n // @PRE All conflicts resolved; envId required when ref is slug.\n // @POST Merge commit created; dialog closed; workspace reloaded.\n async handleContinueUnfinishedMerge() {\n clearError();\n u({ mergeContinueInProgress: true });\n try {\n await gitService.continueMerge(s().dashboardId, '', s().envId);\n s().toast(s().t?.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');\n handlers.closeUnfinishedMergeDialog();\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n await handlers.loadMergeRecoveryState();\n } finally { u({ mergeContinueInProgress: false }); }\n },\n // #endregion handleContinueUnfinishedMerge\n" }, { "contract_id": "getUnfinishedMergeCommandsText", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 249, - "end_line": 256, + "start_line": 309, + "end_line": 316, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -61904,8 +62913,8 @@ "contract_id": "handleCopyUnfinishedMergeCommands", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 258, - "end_line": 271, + "start_line": 318, + "end_line": 331, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -61918,14 +62927,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleCopyUnfinishedMergeCommands [C:1] [TYPE Function]\n // @BRIEF Copy merge recovery commands to clipboard.\n // @PRE commands exist in unfinishedMergeContext.\n // @POST Commands copied to clipboard; toast shown.\n async handleCopyUnfinishedMergeCommands() {\n const text = this.getUnfinishedMergeCommandsText();\n if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }\n u({ copyingUnfinishedMergeCommands: true });\n try {\n await navigator.clipboard.writeText(text);\n s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }\n },\n // #endregion handleCopyUnfinishedMergeCommands\n" + "body": " // #region handleCopyUnfinishedMergeCommands [C:1] [TYPE Function]\n // @BRIEF Copy merge recovery commands to clipboard.\n // @PRE commands exist in unfinishedMergeContext.\n // @POST Commands copied to clipboard; toast shown.\n async handleCopyUnfinishedMergeCommands() {\n const text = handlers.getUnfinishedMergeCommandsText();\n if (!text) { s().toast(s().t?.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning'); return; }\n u({ copyingUnfinishedMergeCommands: true });\n try {\n await navigator.clipboard.writeText(text);\n s().toast(s().t?.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');\n } catch (_e) { s().toast(s().t?.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error'); } finally { u({ copyingUnfinishedMergeCommands: false }); }\n },\n // #endregion handleCopyUnfinishedMergeCommands\n" }, { "contract_id": "handlePull", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 273, - "end_line": 290, + "start_line": 333, + "end_line": 354, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61939,14 +62948,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handlePull [C:2] [TYPE Function]\n // @BRIEF Pull changes from remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Local repository updated with remote changes; workspace reloaded.\n // @SIDE_EFFECT POSTs to /pull; may open merge dialog on GIT_UNFINISHED_MERGE.\n async handlePull() {\n u({ isPulling: true });\n try {\n await gitService.pull(s().dashboardId, s().envId);\n s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');\n await this.loadWorkspace();\n } catch (e) {\n const handled = this.openUnfinishedMergeDialogFromError(e);\n if (handled) await this.loadMergeRecoveryState();\n else s().toast(e.message, 'error');\n } finally { u({ isPulling: false }); }\n },\n // #endregion handlePull\n" + "body": " // #region handlePull [C:2] [TYPE Function]\n // @BRIEF Pull changes from remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Local repository updated with remote changes; workspace reloaded.\n // @SIDE_EFFECT POSTs to /pull; may open merge dialog on GIT_UNFINISHED_MERGE.\n async handlePull() {\n clearError();\n u({ isPulling: true });\n try {\n await gitService.pull(s().dashboardId, s().envId);\n s().toast(s().t?.git?.pull_success || 'Изменения получены из Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n const handled = handlers.openUnfinishedMergeDialogFromError(e);\n if (handled) {\n await handlers.loadMergeRecoveryState();\n } else {\n setGitError(e);\n }\n } finally { u({ isPulling: false }); }\n },\n // #endregion handlePull\n" }, { "contract_id": "handlePush", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 292, - "end_line": 304, + "start_line": 356, + "end_line": 371, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -61959,14 +62968,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handlePush [C:2] [TYPE Function]\n // @BRIEF Push local commits to remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Remote updated with local commits; workspace reloaded.\n async handlePush() {\n u({ isPushing: true });\n try {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ isPushing: false }); }\n },\n // #endregion handlePush\n" + "body": " // #region handlePush [C:2] [TYPE Function]\n // @BRIEF Push local commits to remote repository.\n // @PRE Remote 'origin' configured; envId required when ref is slug.\n // @POST Remote updated with local commits; workspace reloaded.\n async handlePush() {\n clearError();\n u({ isPushing: true });\n try {\n await gitService.push(s().dashboardId, s().envId);\n s().toast(s().t?.git?.push_success || 'Изменения отправлены в Git', 'success');\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ isPushing: false }); }\n },\n // #endregion handlePush\n" }, { "contract_id": "handlePromote", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 306, - "end_line": 327, + "start_line": 373, + "end_line": 397, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -61980,14 +62989,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handlePromote [C:3] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE promoteFromBranch and promoteToBranch differ; direct mode requires reason; envId required when ref is slug.\n // @POST MR created on Git server (MR mode) or direct merge executed (direct mode).\n // @SIDE_EFFECT Opens MR URL in new tab (MR mode); writes policy violation to logs (direct mode).\n async handlePromote() {\n const st = s();\n if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }\n if (st.promoteMode === 'direct' && !String(st.promoteReason || '').trim()) { st.toast('Для небезопасного прямого переноса укажите причину', 'error'); return; }\n u({ promoting: true });\n try {\n const response = await gitService.promote(st.dashboardId, {\n from_branch: st.promoteFromBranch, to_branch: st.promoteToBranch, mode: st.promoteMode,\n title: `Promote ${st.promoteFromBranch} -> ${st.promoteToBranch}: ${st.dashboardTitle || st.dashboardId}`,\n description: st.promoteMode === 'direct' ? `Unsafe direct promote.\\nReason: ${st.promoteReason}` : undefined,\n reason: st.promoteMode === 'direct' ? st.promoteReason : undefined,\n }, st.envId);\n if (st.promoteMode === 'direct') { st.toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning'); }\n else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }\n } catch (e) { st.toast(e.message, 'error'); } finally { u({ promoting: false }); }\n },\n // #endregion handlePromote\n" + "body": " // #region handlePromote [C:3] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE promoteFromBranch and promoteToBranch differ; direct mode requires reason; envId required when ref is slug.\n // @POST MR created on Git server (MR mode) or direct merge executed (direct mode).\n // @SIDE_EFFECT Opens MR URL in new tab (MR mode); writes policy violation to logs (direct mode).\n async handlePromote() {\n const st = s();\n if (!st.promoteFromBranch || !st.promoteToBranch || st.promoteFromBranch === st.promoteToBranch) { st.toast('Выберите разные исходную и целевую ветки', 'error'); return; }\n if (st.promoteMode === 'direct' && !String(st.promoteReason || '').trim()) { st.toast('Для небезопасного прямого переноса укажите причину', 'error'); return; }\n clearError();\n u({ promoting: true });\n try {\n const response = await gitService.promote(st.dashboardId, {\n from_branch: st.promoteFromBranch, to_branch: st.promoteToBranch, mode: st.promoteMode,\n title: `Promote ${st.promoteFromBranch} -> ${st.promoteToBranch}: ${st.dashboardTitle || st.dashboardId}`,\n description: st.promoteMode === 'direct' ? `Unsafe direct promote.\\nReason: ${st.promoteReason}` : undefined,\n reason: st.promoteMode === 'direct' ? st.promoteReason : undefined,\n }, st.envId);\n if (st.promoteMode === 'direct') { st.toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning'); }\n else { if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer'); st.toast('Merge Request создан на Git сервере', 'success'); }\n } catch (e) {\n setGitError(e);\n } finally { u({ promoting: false }); }\n },\n // #endregion handlePromote\n" }, { "contract_id": "openDeployModal", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 329, - "end_line": 342, + "start_line": 399, + "end_line": 412, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -62006,8 +63015,8 @@ "contract_id": "handleCreateRemoteRepo", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 344, - "end_line": 374, + "start_line": 414, + "end_line": 447, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -62021,14 +63030,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleCreateRemoteRepo [C:2] [TYPE Function]\n // @BRIEF Create a remote repository on the selected Git provider.\n // @PRE A Git config is selected; user provides a repo name via prompt.\n // @POST remoteUrl set to created (or existing) repository clone URL.\n // @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.\n async handleCreateRemoteRepo() {\n const config = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }\n if (!s().selectedConfigId && config.id) u({ selectedConfigId: config.id });\n const suggestedName = buildSuggestedRepoName(s().dashboardTitle, s().dashboardId);\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n u({ creatingRemoteRepo: true });\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName, private: true,\n description: `Superset dashboard ${s().dashboardId}: ${s().dashboardTitle || repoName}`,\n auto_init: true, default_branch: 'main',\n });\n const url = repo?.clone_url || repo?.html_url || '';\n if (!url) throw new Error('Remote repository created, but URL is empty');\n u({ remoteUrl: url });\n s().toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) {\n if (e?.status === 409 && /already exists/i.test(String(e?.message || ''))) {\n s().toast(s().t?.git?.repo_already_exists || 'Repository already exists. Enter its URL below and click Init.', 'warning');\n } else { s().toast(e.message, 'error'); }\n } finally { u({ creatingRemoteRepo: false }); }\n },\n // #endregion handleCreateRemoteRepo\n" + "body": " // #region handleCreateRemoteRepo [C:2] [TYPE Function]\n // @BRIEF Create a remote repository on the selected Git provider.\n // @PRE A Git config is selected; user provides a repo name via prompt.\n // @POST remoteUrl set to created (or existing) repository clone URL.\n // @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.\n async handleCreateRemoteRepo() {\n const config = resolveDefaultConfig(s().configs, s().selectedConfigId);\n if (!config) { s().toast(s().t?.git?.init_validation_error || 'Сначала выберите Git сервер', 'error'); return; }\n if (!s().selectedConfigId && config.id) u({ selectedConfigId: config.id });\n const suggestedName = buildSuggestedRepoName(s().dashboardTitle, s().dashboardId);\n const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);\n const repoName = String(inputName || '').trim();\n if (!repoName) return;\n clearError();\n u({ creatingRemoteRepo: true });\n try {\n const repo = await gitService.createRemoteRepository(config.id, {\n name: repoName, private: true,\n description: `Superset dashboard ${s().dashboardId}: ${s().dashboardTitle || repoName}`,\n auto_init: true, default_branch: 'main',\n });\n const url = repo?.clone_url || repo?.html_url || '';\n if (!url) throw new Error('Remote repository created, but URL is empty');\n u({ remoteUrl: url });\n s().toast(`Repository created on ${config.provider}`, 'success');\n } catch (e) {\n if (e?.status === 409 && /already exists/i.test(String(e?.message || ''))) {\n s().toast(s().t?.git?.repo_already_exists || 'Repository already exists. Enter its URL below and click Init.', 'warning', 0);\n } else {\n setGitError(e, 'warning');\n }\n } finally { u({ creatingRemoteRepo: false }); }\n },\n // #endregion handleCreateRemoteRepo\n" }, { "contract_id": "handleInit", "contract_type": "Function", "file_path": "frontend/src/components/git/useGitManager.js", - "start_line": 376, - "end_line": 397, + "start_line": 449, + "end_line": 473, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -62042,7 +63051,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleInit [C:3] [TYPE Function]\n // @BRIEF Initialize local Git repository for the dashboard: clone remote.\n // @PRE selectedConfigId is set; remoteUrl is set; envId required when ref is slug.\n // @POST Repository initialized; initialized=true; workspace loaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/init; loads workspace on success.\n async handleInit() {\n if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }\n // ── Guard: slug-based init requires envId ──\n if (!s().envId && !isNumericDashboardRef(s().dashboardId)) {\n s().toast('Environment must be selected to initialize Git for this dashboard.', 'error');\n return;\n }\n u({ loading: true });\n try {\n await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);\n s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');\n const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';\n u({ initialized: true, repositoryProvider: provider });\n await this.loadWorkspace();\n } catch (e) { s().toast(e.message, 'error'); } finally { u({ loading: false }); }\n },\n // #endregion handleInit\n" + "body": " // #region handleInit [C:3] [TYPE Function]\n // @BRIEF Initialize local Git repository for the dashboard: clone remote.\n // @PRE selectedConfigId is set; remoteUrl is set; envId required when ref is slug.\n // @POST Repository initialized; initialized=true; workspace loaded.\n // @SIDE_EFFECT POSTs to /git/repositories/{ref}/init; loads workspace on success.\n async handleInit() {\n if (!s().selectedConfigId || !s().remoteUrl) { s().toast(s().t?.git?.init_validation_error || 'Заполните все поля', 'error'); return; }\n // ── Guard: slug-based init requires envId ──\n if (!s().envId && !isNumericDashboardRef(s().dashboardId)) {\n s().toast('Environment must be selected to initialize Git for this dashboard.', 'error');\n return;\n }\n clearError();\n u({ loading: true });\n try {\n await gitService.initRepository(s().dashboardId, s().selectedConfigId, s().remoteUrl, s().envId);\n s().toast(s().t?.git?.init_success || 'Репозиторий инициализирован', 'success');\n const provider = resolveDefaultConfig(s().configs, s().selectedConfigId)?.provider || '';\n u({ initialized: true, repositoryProvider: provider });\n await handlers.loadWorkspace();\n } catch (e) {\n setGitError(e);\n } finally { u({ loading: false }); }\n },\n // #endregion handleInit\n" }, { "contract_id": "DocPreview", @@ -62431,8 +63440,8 @@ "contract_id": "handleRunDebug:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/DebugTool.svelte", - "start_line": 46, - "end_line": 87, + "start_line": 56, + "end_line": 97, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62451,8 +63460,8 @@ "contract_id": "startPolling:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/DebugTool.svelte", - "start_line": 89, - "end_line": 119, + "start_line": 99, + "end_line": 129, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62473,7 +63482,7 @@ "contract_type": "Component", "file_path": "frontend/src/components/tools/MapperTool.svelte", "start_line": 1, - "end_line": 236, + "end_line": 352, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -62486,7 +63495,10 @@ "dataset", "sqllab", "excel" - ] + ], + "UX_FEEDBACK": "Toast notifications on success/error.", + "UX_RECOVERY": "Fix validation errors, retry operation.", + "UX_STATE": "Error -> Validation errors shown inline, toast notifications displayed." }, "relations": [ { @@ -62499,14 +63511,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n
\n \n
\n
\n
\n ({ value: e.id, label: e.name }))\n ]}\n />\n
\n
\n \n
\n
\n\n
\n {$t.mapper.source}\n
\n \n \n
\n
\n\n {#if source === 'sqllab'}\n
\n
\n \n
\n
\n \n
\n
\n {:else}\n
\n \n
\n {/if}\n\n
\n \n {#if isGeneratingDocs}\n {$t.mapper?.generating}\n {:else}\n {$t.datasets?.generate_docs}\n {/if}\n \n \n {isRunning ? $t.mapper.starting : $t.mapper.run}\n \n
\n
\n
\n\n generatedDoc = null}\n onSave={handleApplyDoc}\n />\n
\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n \n
\n
\n
\n \n
\n
\n

{$t.mapper.title}

\n

Настройте параметры маппинга колонок

\n
\n
\n
\n\n \n
\n \n
\n
\n \n ({ value: e.id, label: e.name }))\n ]}\n />\n
\n
\n \n \n
\n
\n\n \n
\n \n
\n \n \n
\n
\n\n \n
\n {#if source === 'sqllab'}\n
\n
\n \n

Настройки SQL Lab

\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n {:else}\n
\n
\n \n

Настройки Excel

\n
\n
\n \n \n
\n
\n {/if}\n
\n\n \n
\n \n {#if isGeneratingDocs}\n \n \n \n \n {$t.mapper?.generating}\n {:else}\n \n {$t.datasets?.generate_docs}\n {/if}\n \n \n {#if isRunning}\n \n \n \n \n {$t.mapper.starting}\n {:else}\n \n \n \n {$t.mapper.run}\n {/if}\n \n
\n
\n
\n\n \n generatedDoc = null}\n onSave={handleApplyDoc}\n />\n
\n\n\n" }, { "contract_id": "fetchData:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 33, - "end_line": 44, + "start_line": 38, + "end_line": 56, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62518,14 +63530,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region fetchData:Function [TYPE Function]\n // @PURPOSE: Fetches environments.\n // @PRE: None.\n // @POST: envs array is populated.\n async function fetchData() {\n try {\n envs = await api.fetchApi('/environments');\n } catch (e) {\n addToast($t.mapper.errors.fetch_failed, 'error');\n }\n }\n // #endregion fetchData:Function\n" + "body": " // #region fetchData:Function [TYPE Function]\n // @PURPOSE: Fetches environments.\n // @PRE: None.\n // @POST: envs array is populated.\n async function fetchData() {\n try {\n envs = await api.fetchApi('/environments');\n // Pre-fill with active environment from global context\n const { get } = await import('svelte/store');\n const envCtx = await import('$lib/stores/environmentContext.js');\n const state = get(envCtx.environmentContextStore);\n if (state.selectedEnvId && envs.some((e) => e.id === state.selectedEnvId)) {\n selectedEnv = state.selectedEnvId;\n }\n } catch (e) {\n addToast($t.mapper.errors.fetch_failed, 'error');\n }\n }\n // #endregion fetchData:Function\n" }, { "contract_id": "handleRunMapper:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 46, - "end_line": 86, + "start_line": 58, + "end_line": 98, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62543,8 +63555,8 @@ "contract_id": "handleGenerateDocs:Function", "contract_type": "Function", "file_path": "frontend/src/components/tools/MapperTool.svelte", - "start_line": 88, - "end_line": 123, + "start_line": 100, + "end_line": 135, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -62583,7 +63595,7 @@ "contract_type": "Module", "file_path": "frontend/src/lib/api.js", "start_line": 1, - "end_line": 246, + "end_line": 277, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -62608,7 +63620,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region ApiModule [C:3] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling]\n// @BRIEF Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback.\n// @RELATION DEPENDS_ON -> [ToastsModule]\n// @RELATION CALLED_BY -> [TranslateApi]\nimport { addToast } from './toasts.js';\n\nconst API_BASE_URL = '/api';\n\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (typeof detail === 'string' ? detail : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail)))\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n\nfunction notifyApiError(error) {\n if (error?.status === 401) { addToast(`401 Unauthorized: ${error.message}`, 'error'); return; }\n if (error?.status >= 500) { addToast(`Server error (${error.status}): ${error.message}`, 'error'); return; }\n addToast(error.message, 'error');\n}\n\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/status');\n const isNoRepoError = (error?.status === 400 || error?.status === 404) && /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n const isGitPullEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/pull');\n const isUnfinishedMergeError = error?.status === 409 && (String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' || String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE');\n const isDatasetClarificationEndpoint = typeof endpoint === 'string' && /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession = error?.status === 404 && /Clarification session not found/i.test(String(error?.message || ''));\n const isGitRepoEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/');\n const isMissingEnvIdError = error?.status === 400 && /env_id is required/i.test(String(error?.message || ''));\n const isGitConfigReposEndpoint = typeof endpoint === 'string' && /^\\/git\\/config\\/[^/]+\\/repositories$/.test(endpoint);\n const isRepoAlreadyExistsError = error?.status === 409 && /already exists/i.test(String(error?.message || ''));\n return (isGitStatusEndpoint && isNoRepoError) || (isGitPullEndpoint && isUnfinishedMergeError) || (isDatasetClarificationEndpoint && isMissingClarificationSession) || (isGitRepoEndpoint && isMissingEnvIdError) || (isGitConfigReposEndpoint && isRepoAlreadyExistsError);\n}\n\nexport const getWsUrl = (taskId) => {\n const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';\n return `${protocol}//${host}/ws/logs/${taskId}`;\n};\n\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = { 'Content-Type': 'application/json', ...extraHeaders };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) headers['Authorization'] = `Bearer ${token}`;\n }\n return headers;\n}\n\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from endpoint=${endpoint}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) throw await buildApiError(response);\n return await response.blob();\n } catch (error) {\n if (notifyError) notifyApiError(error);\n throw error;\n }\n}\n\nasync function postApi(endpoint, body, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST', headers: getAuthHeaders(options.headers || {}), body: JSON.stringify(body),\n });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function deleteApi(endpoint, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'DELETE', headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n const fetchOptions = { method, headers: getAuthHeaders(requestOptions.headers || {}) };\n if (body) fetchOptions.body = JSON.stringify(body);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] ${method} ${endpoint}:`, error);\n if (!shouldSuppressApiErrorToast(endpoint, error)) notifyApiError(error);\n throw error;\n }\n}\n\nexport const api = {\n fetchApi, postApi, deleteApi, requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) options.plugin_id.forEach((pid) => params.append('plugin_id', pid));\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n return fetchApi(`/tasks/${taskId}/logs${params.toString() ? `?${params.toString()}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const eid = String(environmentId || '').trim();\n if (!eid) throw new Error('environmentId is required for Superset account lookup');\n const params = new URLSearchParams({ environment_id: eid });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (s) => requestApi('/settings/global', 'PATCH', s),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, s) => requestApi(`/environments/${id}/schedule`, 'PUT', s),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (s) => requestApi('/settings/storage', 'PUT', s),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (p) => postApi('/llm/providers/fetch-models', p),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) => fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n if (options.override_show_all != null) params.append('override_show_all', String(Boolean(options.override_show_all)));\n if (options.filters?.title) for (const v of options.filters.title) params.append('filter_title', v);\n if (options.filters?.git_status) for (const v of options.filters.git_status) params.append('filter_git_status', v);\n if (options.filters?.llm_status) for (const v of options.filters.llm_status) params.append('filter_llm_status', v);\n if (options.filters?.changed_on) for (const v of options.filters.changed_on) params.append('filter_changed_on', v);\n if (options.filters?.actor) for (const v of options.filters.actor) params.append('filter_actor', v);\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, ref) => fetchApi(`/dashboards/${encodeURIComponent(String(ref))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, ref, opts = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (opts.limit) params.append('limit', opts.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(ref))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, ref, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.force != null) params.append('force', String(Boolean(opts.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(ref))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (src, tgt) => fetchApi(`/dashboards/db-mappings?source_env_id=${src}&target_env_id=${tgt}`),\n calculateMigrationDryRun: (p) => postApi('/migration/dry-run', p),\n getDatasets: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n if (opts.filter) params.append('filter', opts.filter);\n if (opts.page) params.append('page', opts.page);\n if (opts.page_size) params.append('page_size', opts.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (s) => requestApi('/settings/consolidated', 'PATCH', s),\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (p) => postApi('/settings/automation/policies', p),\n updateValidationPolicy: (id, p) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', p),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n // ── API Key Management ────────────────────────────────────\n listApiKeys: () => fetchApi('/admin/api-keys/'),\n createApiKey: (payload) => postApi('/admin/api-keys/', payload, { suppressToast: true }),\n revokeApiKey: (keyId) => requestApi(`/admin/api-keys/${keyId}`, 'DELETE'),\n};\n// #endregion ApiModule\n" + "body": "// #region ApiModule [C:3] [TYPE Module] [SEMANTICS api, client, fetch, auth, error-handling]\n// @BRIEF Handles all communication with the backend API via fetch wrappers with auth, error normalization, and toast feedback.\n// @RELATION DEPENDS_ON -> [ToastsModule]\n// @RELATION CALLED_BY -> [TranslateApi]\nimport { addToast } from './toasts.js';\n\nconst API_BASE_URL = '/api';\n\nasync function buildApiError(response) {\n const errorData = await response.json().catch(() => ({}));\n const detail = errorData?.detail;\n const message = detail\n ? (typeof detail === 'string' ? detail : (typeof detail?.message === 'string' ? detail.message : JSON.stringify(detail)))\n : `API request failed with status ${response.status}`;\n const error = new Error(message);\n /** @type {any} */ (error).status = response.status;\n /** @type {any} */ (error).detail = detail;\n if (detail && typeof detail === 'object' && detail.error_code) {\n /** @type {any} */ (error).error_code = String(detail.error_code);\n }\n return error;\n}\n\nfunction notifyApiError(error) {\n if (error?.status === 401) { addToast(`401 Unauthorized: ${error.message}`, 'error'); return; }\n if (error?.status >= 500) { addToast(`Server error (${error.status}): ${error.message}`, 'error'); return; }\n addToast(error.message, 'error');\n}\n\nfunction shouldSuppressApiErrorToast(endpoint, error) {\n const isGitStatusEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/status');\n const isNoRepoError = (error?.status === 400 || error?.status === 404) && /Repository for dashboard .* not found/i.test(String(error?.message || ''));\n const isGitPullEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/') && endpoint.endsWith('/pull');\n const isUnfinishedMergeError = error?.status === 409 && (String(error?.error_code || '') === 'GIT_UNFINISHED_MERGE' || String(error?.detail?.error_code || '') === 'GIT_UNFINISHED_MERGE');\n const isDatasetClarificationEndpoint = typeof endpoint === 'string' && /\\/dataset-orchestration\\/sessions\\/[^/]+\\/clarification$/.test(endpoint);\n const isMissingClarificationSession = error?.status === 404 && /Clarification session not found/i.test(String(error?.message || ''));\n const isGitRepoEndpoint = typeof endpoint === 'string' && endpoint.startsWith('/git/repositories/');\n const isMissingEnvIdError = error?.status === 400 && /env_id is required/i.test(String(error?.message || ''));\n const isGitConfigReposEndpoint = typeof endpoint === 'string' && /^\\/git\\/config\\/[^/]+\\/repositories$/.test(endpoint);\n const isRepoAlreadyExistsError = error?.status === 409 && /already exists/i.test(String(error?.message || ''));\n return (isGitStatusEndpoint && isNoRepoError) || (isGitPullEndpoint && isUnfinishedMergeError) || (isDatasetClarificationEndpoint && isMissingClarificationSession) || (isGitRepoEndpoint && isMissingEnvIdError) || (isGitConfigReposEndpoint && isRepoAlreadyExistsError);\n}\n\n/**\n * Build a WebSocket URL for a task's log stream, including auth token.\n * The token is appended as a query param because the WebSocket API\n * does not allow custom headers in the browser.\n */\nexport const getWsUrl = (taskId) => {\n const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const host = typeof window !== 'development' && typeof window !== 'undefined' ? window.location.host : 'localhost:8000';\n let url = `${protocol}//${host}/ws/logs/${taskId}`;\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n url += `?token=${encodeURIComponent(token)}`;\n }\n }\n return url;\n};\n\n/**\n * Build a WebSocket URL for translation run progress streaming.\n * The token is appended as a query param for WebSocket auth.\n */\nexport const getTranslateRunWsUrl = (runId) => {\n const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const host = typeof window !== 'undefined' ? window.location.host : 'localhost:8000';\n let url = `${protocol}//${host}/ws/translate/run/${runId}`;\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) {\n url += `?token=${encodeURIComponent(token)}`;\n }\n }\n return url;\n};\n\nfunction getAuthHeaders(extraHeaders = {}) {\n const headers = { 'Content-Type': 'application/json', ...extraHeaders };\n if (typeof window !== 'undefined') {\n const token = localStorage.getItem('auth_token');\n if (token) headers['Authorization'] = `Bearer ${token}`;\n }\n return headers;\n}\n\nasync function fetchApi(endpoint, options = {}) {\n try {\n console.log(`[api.fetchApi][Action] Fetching from endpoint=${endpoint}`);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.fetchApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function fetchApiBlob(endpoint, options = {}) {\n const notifyError = options.notifyError !== false;\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { headers: getAuthHeaders(options.headers || {}) });\n if (response.status === 202) {\n const payload = await response.json().catch(() => ({ message: \"Resource is being prepared\" }));\n const error = new Error(payload?.message || \"Resource is being prepared\");\n /** @type {any} */ (error).status = 202;\n throw error;\n }\n if (!response.ok) throw await buildApiError(response);\n return await response.blob();\n } catch (error) {\n if (notifyError) notifyApiError(error);\n throw error;\n }\n}\n\nasync function postApi(endpoint, body, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, {\n method: 'POST', headers: getAuthHeaders(options.headers || {}), body: JSON.stringify(body),\n });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.postApi][Coherence:Failed] ${endpoint}:`, error);\n if (!options.suppressToast) notifyApiError(error);\n throw error;\n }\n}\n\nasync function deleteApi(endpoint, options = {}) {\n try {\n const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'DELETE', headers: getAuthHeaders(options.headers || {}) });\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.deleteApi][Coherence:Failed] ${endpoint}:`, error);\n notifyApiError(error);\n throw error;\n }\n}\n\nasync function requestApi(endpoint, method = 'GET', body = null, requestOptions = {}) {\n try {\n const fetchOptions = { method, headers: getAuthHeaders(requestOptions.headers || {}) };\n if (body) fetchOptions.body = JSON.stringify(body);\n const response = await fetch(`${API_BASE_URL}${endpoint}`, fetchOptions);\n if (!response.ok) throw await buildApiError(response);\n if (response.status === 204) return null;\n return await response.json();\n } catch (error) {\n console.error(`[api.requestApi][Coherence:Failed] ${method} ${endpoint}:`, error);\n if (!requestOptions.suppressToast && !shouldSuppressApiErrorToast(endpoint, error)) {\n notifyApiError(error);\n }\n throw error;\n }\n}\n\nexport const api = {\n fetchApi, postApi, deleteApi, requestApi,\n getPlugins: () => fetchApi('/plugins'),\n getTasks: (options = {}) => {\n const params = new URLSearchParams();\n if (options.limit != null) params.append('limit', String(options.limit));\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.status) params.append('status', options.status);\n if (options.task_type) params.append('task_type', options.task_type);\n if (options.completed_only != null) params.append('completed_only', String(Boolean(options.completed_only)));\n if (Array.isArray(options.plugin_id)) options.plugin_id.forEach((pid) => params.append('plugin_id', pid));\n const query = params.toString();\n return fetchApi(`/tasks${query ? `?${query}` : ''}`);\n },\n getTask: (taskId) => fetchApi(`/tasks/${taskId}`),\n getTaskLogs: (taskId, options = {}) => {\n const params = new URLSearchParams();\n if (options.level) params.append('level', options.level);\n if (options.source) params.append('source', options.source);\n if (options.search) params.append('search', options.search);\n if (options.offset != null) params.append('offset', String(options.offset));\n if (options.limit != null) params.append('limit', String(options.limit));\n return fetchApi(`/tasks/${taskId}/logs${params.toString() ? `?${params.toString()}` : ''}`);\n },\n createTask: (pluginId, params) => postApi('/tasks', { plugin_id: pluginId, params }),\n getProfilePreferences: () => fetchApi('/profile/preferences'),\n updateProfilePreferences: (payload) => requestApi('/profile/preferences', 'PATCH', payload),\n lookupSupersetAccounts: (environmentId, options = {}) => {\n const eid = String(environmentId || '').trim();\n if (!eid) throw new Error('environmentId is required for Superset account lookup');\n const params = new URLSearchParams({ environment_id: eid });\n if (options.search) params.append('search', options.search);\n if (options.page_index != null) params.append('page_index', String(options.page_index));\n if (options.page_size != null) params.append('page_size', String(options.page_size));\n if (options.sort_column) params.append('sort_column', options.sort_column);\n if (options.sort_order) params.append('sort_order', options.sort_order);\n return fetchApi(`/profile/superset-accounts?${params.toString()}`);\n },\n getSettings: () => fetchApi('/settings'),\n updateGlobalSettings: (s) => requestApi('/settings/global', 'PATCH', s),\n getEnvironments: () => fetchApi('/settings/environments'),\n addEnvironment: (env) => postApi('/settings/environments', env),\n updateEnvironment: (id, env) => requestApi(`/settings/environments/${id}`, 'PUT', env),\n deleteEnvironment: (id) => requestApi(`/settings/environments/${id}`, 'DELETE'),\n testEnvironmentConnection: (id) => postApi(`/settings/environments/${id}/test`, {}),\n updateEnvironmentSchedule: (id, s) => requestApi(`/environments/${id}/schedule`, 'PUT', s),\n getStorageSettings: () => fetchApi('/settings/storage'),\n updateStorageSettings: (s) => requestApi('/settings/storage', 'PUT', s),\n getEnvironmentsList: () => fetchApi('/environments'),\n getLlmStatus: () => fetchApi('/llm/status'),\n fetchLlmModels: (p) => postApi('/llm/providers/fetch-models', p),\n getEnvironmentDatabases: (id) => fetchApi(`/environments/${id}/databases`),\n getStorageFileBlob: (path) => fetchApiBlob(`/storage/file?path=${encodeURIComponent(path)}`),\n getDashboards: (envId, options = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (options.search) params.append('search', options.search);\n if (options.page) params.append('page', options.page);\n if (options.page_size) params.append('page_size', options.page_size);\n if (options.page_context) params.append('page_context', options.page_context);\n if (options.apply_profile_default != null) params.append('apply_profile_default', String(Boolean(options.apply_profile_default)));\n if (options.override_show_all != null) params.append('override_show_all', String(Boolean(options.override_show_all)));\n if (options.filters?.title) for (const v of options.filters.title) params.append('filter_title', v);\n if (options.filters?.git_status) for (const v of options.filters.git_status) params.append('filter_git_status', v);\n if (options.filters?.llm_status) for (const v of options.filters.llm_status) params.append('filter_llm_status', v);\n if (options.filters?.changed_on) for (const v of options.filters.changed_on) params.append('filter_changed_on', v);\n if (options.filters?.actor) for (const v of options.filters.actor) params.append('filter_actor', v);\n return fetchApi(`/dashboards?${params.toString()}`);\n },\n getDashboardDetail: (envId, ref) => fetchApi(`/dashboards/${encodeURIComponent(String(ref))}?env_id=${envId}`),\n getDashboardTaskHistory: (envId, ref, opts = {}) => {\n const params = new URLSearchParams();\n if (envId) params.append('env_id', envId);\n if (opts.limit) params.append('limit', opts.limit);\n return fetchApi(`/dashboards/${encodeURIComponent(String(ref))}/tasks?${params.toString()}`);\n },\n getDashboardThumbnail: (envId, ref, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.force != null) params.append('force', String(Boolean(opts.force)));\n return fetchApiBlob(`/dashboards/${encodeURIComponent(String(ref))}/thumbnail?${params.toString()}`, { notifyError: false });\n },\n getDatabaseMappings: (src, tgt) => fetchApi(`/dashboards/db-mappings?source_env_id=${src}&target_env_id=${tgt}`),\n calculateMigrationDryRun: (p) => postApi('/migration/dry-run', p),\n getDatasets: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n if (opts.filter) params.append('filter', opts.filter);\n if (opts.page) params.append('page', opts.page);\n if (opts.page_size) params.append('page_size', opts.page_size);\n return fetchApi(`/datasets?${params.toString()}`);\n },\n getDatasetIds: (envId, opts = {}) => {\n const params = new URLSearchParams({ env_id: envId });\n if (opts.search) params.append('search', opts.search);\n return fetchApi(`/datasets/ids?${params.toString()}`);\n },\n getDatasetDetail: (envId, datasetId) => fetchApi(`/datasets/${datasetId}?env_id=${envId}`),\n getConsolidatedSettings: () => fetchApi('/settings/consolidated'),\n updateConsolidatedSettings: (s) => requestApi('/settings/consolidated', 'PATCH', s),\n getValidationPolicies: () => fetchApi('/settings/automation/policies'),\n createValidationPolicy: (p) => postApi('/settings/automation/policies', p),\n updateValidationPolicy: (id, p) => requestApi(`/settings/automation/policies/${id}`, 'PATCH', p),\n deleteValidationPolicy: (id) => requestApi(`/settings/automation/policies/${id}`, 'DELETE'),\n getTranslationSchedules: () => fetchApi('/settings/automation/translation-schedules'),\n getHealthSummary: (environmentId) => {\n const query = environmentId ? `?env_id=${encodeURIComponent(environmentId)}` : '';\n return fetchApi(`/health/summary${query}`, { suppressToast: true });\n },\n // ── API Key Management ────────────────────────────────────\n listApiKeys: () => fetchApi('/admin/api-keys/'),\n createApiKey: (payload) => postApi('/admin/api-keys/', payload, { suppressToast: true }),\n revokeApiKey: (keyId) => requestApi(`/admin/api-keys/${keyId}`, 'DELETE'),\n};\n// #endregion ApiModule\n" }, { "contract_id": "ReportsApiTest", @@ -64831,7 +65843,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

\n {$t.dataset_review?.source?.eyebrow}\n

\n

\n {$t.dataset_review?.source?.title}\n

\n

\n {$t.dataset_review?.source?.description}\n

\n
\n\n \n {#if intakeState === \"Rejected\"}\n {$t.dataset_review?.source?.state_rejected}\n {:else if intakeState === \"Validating\"}\n {$t.dataset_review?.source?.state_validating}\n {:else}\n {$t.dataset_review?.source?.state_idle}\n {/if}\n
\n
\n\n
\n setMode(\"superset_link\")}\n >\n
\n {$t.dataset_review?.source?.superset_link_tab}\n
\n
\n {$t.dataset_review?.source?.superset_link_tab_hint}\n
\n \n\n setMode(\"dataset_selection\")}\n >\n
\n {$t.dataset_review?.source?.dataset_selection_tab}\n
\n
\n {$t.dataset_review?.source?.dataset_selection_tab_hint}\n
\n \n
\n\n
\n Progress: {progressionLabel}. После старта сессии можно продолжить review даже при частично восстановленном контексте.\n
\n\n
\n
\n \n\n \n
\n\n \n {getInlineHint()}\n \n\n
\n
\n {#if isSupersetLinkMode}\n {$t.dataset_review?.source?.superset_link_recovery_note}\n {:else}\n {$t.dataset_review?.source?.dataset_selection_recovery_note}\n {/if}\n
\n Долгие шаги восстановления выполняются асинхронно: статус и следующий шаг будут видны в workspace.\n
\n
\n\n \n {#if submitting}\n {$t.dataset_review?.source?.submitting}\n {:else if isSupersetLinkMode}\n {$t.dataset_review?.source?.submit_superset_link}\n {:else}\n {$t.dataset_review?.source?.submit_dataset_selection}\n {/if}\n \n
\n \n\n\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

\n {$t.dataset_review?.source?.eyebrow}\n

\n

\n {$t.dataset_review?.source?.title}\n

\n

\n {$t.dataset_review?.source?.description}\n

\n
\n\n \n {#if intakeState === \"Rejected\"}\n {$t.dataset_review?.source?.state_rejected}\n {:else if intakeState === \"Validating\"}\n {$t.dataset_review?.source?.state_validating}\n {:else}\n {$t.dataset_review?.source?.state_idle}\n {/if}\n
\n
\n\n
\n setMode(\"superset_link\")}\n >\n
\n {$t.dataset_review?.source?.superset_link_tab}\n
\n
\n {$t.dataset_review?.source?.superset_link_tab_hint}\n
\n \n\n setMode(\"dataset_selection\")}\n >\n
\n {$t.dataset_review?.source?.dataset_selection_tab}\n
\n
\n {$t.dataset_review?.source?.dataset_selection_tab_hint}\n
\n \n
\n\n
\n Progress: {progressionLabel}. После старта сессии можно продолжить review даже при частично восстановленном контексте.\n
\n\n
\n
\n \n\n \n
\n\n \n {getInlineHint()}\n \n\n
\n
\n {#if isSupersetLinkMode}\n {$t.dataset_review?.source?.superset_link_recovery_note}\n {:else}\n {$t.dataset_review?.source?.dataset_selection_recovery_note}\n {/if}\n
\n {$t.dataset_review?.source?.async_recovery_hint}\n
\n
\n\n \n {#if submitting}\n {$t.dataset_review?.source?.submitting}\n {:else if isSupersetLinkMode}\n {$t.dataset_review?.source?.submit_superset_link}\n {:else}\n {$t.dataset_review?.source?.submit_dataset_selection}\n {/if}\n \n
\n \n\n\n\n" }, { "contract_id": "ValidationFindingsPanel", @@ -64875,7 +65887,7 @@ "contract_type": "Function", "file_path": "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js", "start_line": 4, - "end_line": 160, + "end_line": 161, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -64905,7 +65917,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region SourceIntakePanelUxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, source-intake, ux-tests, validation, recovery\n// @PURPOSE: Verify source intake entry paths, validation feedback, and submit payload behavior for US1.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [SourceIntakePanel]\n// @UX_STATE: Idle -> Default intake renders both entry paths and waits for user input.\n// @UX_STATE: Validating -> Inline feedback confirms recognized links and local payload validation.\n// @UX_STATE: Rejected -> Invalid source input remains local and exposes recovery guidance.\n// @TEST_CONTRACT: SourceIntakePanelProps -> ObservableIntakeUX\n// @TEST_SCENARIO: invalid_superset_link_shows_rejected_state -> Invalid URL input keeps submit local and shows rejection feedback.\n// @TEST_SCENARIO: recognized_superset_link_submits_payload -> Recognized link path submits normalized payload.\n// @TEST_SCENARIO: dataset_selection_mode_changes_cta -> Dataset selection path switches CTA and payload source kind.\n// @TEST_EDGE: missing_environment -> Required environment guard blocks submit.\n// @TEST_EDGE: invalid_type -> Invalid Superset URL shows recovery hint.\n// @TEST_EDGE: external_fail -> Submit callback failure is rendered inline.\n// @TEST_INVARIANT: intake_contract_remains_observable -> VERIFIED_BY: [invalid_superset_link_shows_rejected_state, recognized_superset_link_submits_payload, dataset_selection_mode_changes_cta]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport SourceIntakePanel from \"../SourceIntakePanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n choose_environment: \"Choose environment\",\n error: \"Common error\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"SourceIntakePanel UX Contract\", () => {\n const environments = [{ id: \"env-1\", name: \"DEV\" }];\n\n it(\"invalid_superset_link_shows_rejected_state\", async () => {\n const onsubmit = vi.fn();\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"not-a-url\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).not.toHaveBeenCalled();\n expect(screen.getByText(\"Rejected\")).toBeDefined();\n expect(screen.getByText(\"Superset link must start with http\")).toBeDefined();\n });\n\n it(\"recognized_superset_link_submits_payload\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n acknowledgment: \"Recognized Superset link\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/superset/dashboard/42 \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"https://demo.local/superset/dashboard/42\",\n source_kind: \"superset_link\",\n });\n expect(screen.getByText(\"Recognized Superset link\")).toBeDefined();\n });\n\n it(\"dataset_selection_mode_changes_cta\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.click(\n screen.getByRole(\"button\", {\n name: \"Dataset selection Enter dataset ref\",\n }),\n );\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"public.sales\"), {\n target: { value: \" public.sales \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"public.sales\",\n source_kind: \"dataset_selection\",\n });\n });\n\n it(\"external_fail_renders_inline_error\", async () => {\n const onsubmit = vi.fn().mockRejectedValue(new Error(\"Backend rejected source\"));\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/dashboard/42\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(screen.getByText(\"Backend rejected source\")).toBeDefined();\n });\n});\n// #endregion SourceIntakePanelUxTests:Module\n" + "body": "// #region SourceIntakePanelUxTests:Module [TYPE Function]\n// @SEMANTICS: dataset-review, source-intake, ux-tests, validation, recovery\n// @PURPOSE: Verify source intake entry paths, validation feedback, and submit payload behavior for US1.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [SourceIntakePanel]\n// @UX_STATE: Idle -> Default intake renders both entry paths and waits for user input.\n// @UX_STATE: Validating -> Inline feedback confirms recognized links and local payload validation.\n// @UX_STATE: Rejected -> Invalid source input remains local and exposes recovery guidance.\n// @TEST_CONTRACT: SourceIntakePanelProps -> ObservableIntakeUX\n// @TEST_SCENARIO: invalid_superset_link_shows_rejected_state -> Invalid URL input keeps submit local and shows rejection feedback.\n// @TEST_SCENARIO: recognized_superset_link_submits_payload -> Recognized link path submits normalized payload.\n// @TEST_SCENARIO: dataset_selection_mode_changes_cta -> Dataset selection path switches CTA and payload source kind.\n// @TEST_EDGE: missing_environment -> Required environment guard blocks submit.\n// @TEST_EDGE: invalid_type -> Invalid Superset URL shows recovery hint.\n// @TEST_EDGE: external_fail -> Submit callback failure is rendered inline.\n// @TEST_INVARIANT: intake_contract_remains_observable -> VERIFIED_BY: [invalid_superset_link_shows_rejected_state, recognized_superset_link_submits_payload, dataset_selection_mode_changes_cta]\n\nimport { describe, it, expect, vi } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/svelte\";\nimport SourceIntakePanel from \"../SourceIntakePanel.svelte\";\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n choose_environment: \"Choose environment\",\n error: \"Common error\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n async_recovery_hint: \"Recovery steps run async\",\n },\n },\n });\n return () => {};\n },\n },\n}));\n\ndescribe(\"SourceIntakePanel UX Contract\", () => {\n const environments = [{ id: \"env-1\", name: \"DEV\" }];\n\n it(\"invalid_superset_link_shows_rejected_state\", async () => {\n const onsubmit = vi.fn();\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"not-a-url\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).not.toHaveBeenCalled();\n expect(screen.getByText(\"Rejected\")).toBeDefined();\n expect(screen.getByText(\"Superset link must start with http\")).toBeDefined();\n });\n\n it(\"recognized_superset_link_submits_payload\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n acknowledgment: \"Recognized Superset link\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/superset/dashboard/42 \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"https://demo.local/superset/dashboard/42\",\n source_kind: \"superset_link\",\n });\n expect(screen.getByText(\"Recognized Superset link\")).toBeDefined();\n });\n\n it(\"dataset_selection_mode_changes_cta\", async () => {\n const onsubmit = vi.fn().mockResolvedValue(undefined);\n const { container } = render(SourceIntakePanel, {\n environments,\n onsubmit,\n });\n\n await fireEvent.click(\n screen.getByRole(\"button\", {\n name: \"Dataset selection Enter dataset ref\",\n }),\n );\n await fireEvent.change(screen.getByRole(\"combobox\"), {\n target: { value: \"env-1\" },\n });\n await fireEvent.input(screen.getByPlaceholderText(\"public.sales\"), {\n target: { value: \" public.sales \" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(onsubmit).toHaveBeenCalledWith({\n environment_id: \"env-1\",\n source_input: \"public.sales\",\n source_kind: \"dataset_selection\",\n });\n });\n\n it(\"external_fail_renders_inline_error\", async () => {\n const onsubmit = vi.fn().mockRejectedValue(new Error(\"Backend rejected source\"));\n const { container } = render(SourceIntakePanel, {\n environments,\n selectedEnvironmentId: \"env-1\",\n onsubmit,\n });\n\n await fireEvent.input(screen.getByPlaceholderText(\"https://superset.local/dashboard/10\"), {\n target: { value: \"https://demo.local/dashboard/42\" },\n });\n await fireEvent.submit(container.querySelector(\"form\"));\n\n expect(screen.getByText(\"Backend rejected source\")).toBeDefined();\n });\n});\n// #endregion SourceIntakePanelUxTests:Module\n" }, { "contract_id": "DatasetReviewUs2WorkspaceUxTests:Module", @@ -65189,8 +66201,8 @@ "contract_id": "disconnectWebSocket:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 451, - "end_line": 466, + "start_line": 449, + "end_line": 464, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65216,8 +66228,8 @@ "contract_id": "loadRecentTasks:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 468, - "end_line": 491, + "start_line": 466, + "end_line": 489, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65235,8 +66247,8 @@ "contract_id": "selectTask:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 493, - "end_line": 506, + "start_line": 491, + "end_line": 504, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65254,8 +66266,8 @@ "contract_id": "goBackToList:Function", "contract_type": "Function", "file_path": "frontend/src/lib/components/layout/TaskDrawer.svelte", - "start_line": 508, - "end_line": 526, + "start_line": 506, + "end_line": 524, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -65931,6 +66943,24 @@ "tier_source": "AutoCalculated", "body": "\n\n\n\n\n\n\n{#if show}\n\t\n\t\t e.stopPropagation()}\n\t\t\trole=\"dialog\"\n\t\t\taria-modal=\"true\"\n\t\t\taria-label={$t.translate?.bulk_replace?.title}\n\t\t>\n\t\t\t\n\t\t\t
\n\t\t\t\t

{$t.translate?.bulk_replace?.title}

\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'configuring' || uxState === 'previewing' || uxState === 'preview_error'}\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t e.key === 'Enter' && handlePreview()}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t e.key === 'Enter' && handlePreview()}\n\t\t\t\t\t\t/>\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{#each targetLanguages as lang}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.bulk_replace?.preview_affected}\n\t\t\t\t\t\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'preview_error'}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{errorMessage}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'previewing' && affectedRecords.length > 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.preview_count.replace('{count}', affectedRecords.length)}\n\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.apply_changes}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#each affectedRecords.slice(0, 100) as row, i}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
{$t.translate?.bulk_replace?.table_number}{$t.translate?.bulk_replace?.table_before}{$t.translate?.bulk_replace?.table_after}
{i + 1}{row.final_value || row.translated_value || ''}{previewAfter(row)}
\n\t\t\t\t\t\t\t{#if affectedRecords.length > 100}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.showing_first.replace('{shown}', 100).replace('{total}', affectedRecords.length)}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t{#if uxState === 'previewing' && affectedRecords.length === 0}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{$t.translate?.bulk_replace?.no_matching}

\n\t\t\t\t\t\t

{$t.translate?.bulk_replace?.no_matching_hint}

\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'confirming'}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.confirm_title.replace('{count}', applyCount)}\n\t\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t

\n\t\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.confirm_body.replace('{language}', targetLanguage)}\n\t\t\t\t\t\t\t\t

\n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{#if submitToDict}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t
\n\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.cancel}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.apply_count.replace('{count}', applyCount)}\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'applying'}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.bulk_replace?.applying}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'applied'}\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t

{$t.translate?.bulk_replace?.applied_title}

\n\t\t\t\t\t\t

{$t.translate?.bulk_replace?.applied_body.replace('{count}', applyCount)}

\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.done}\n\t\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t{#if uxState === 'apply_error'}\n\t\t\t\t\t
\n\t\t\t\t\t\t

{errorMessage}

\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.retry}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{$t.translate?.bulk_replace?.cancel}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t
\n\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t{$t.translate?.bulk_replace?.close}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\n{/if}\n\n" }, + { + "contract_id": "ConfigTabForm", + "contract_type": "Component", + "file_path": "frontend/src/lib/components/translate/ConfigTabForm.svelte", + "start_line": 1, + "end_line": 532, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "BRIEF": "Configuration tab content for Translation Job — datasource selection with auto-detected", + "COMPLEXITY": 4 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n

{$t.translate?.config?.basic_info}

\n
\n
\n \n \n {#if validationErrors.name}\n

{validationErrors.name}

\n {/if}\n
\n
\n \n \n
\n
\n
\n\n \n
\n

{$t.translate?.config?.datasource}

\n
\n
\n \n \n \n {#each environments as env}\n \n {/each}\n \n
\n
\n \n searchDatasources('')}\n onblur={() => setTimeout(() => (showDatasourceDropdown = false), 150)}\n class=\"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring\"\n placeholder={$t.translate?.config?.datasource_search_placeholder || 'Search datasets...'}\n />\n {#if datasourceLoading}\n
\n {/if}\n {#if showDatasourceDropdown}\n
\n {#each datasourceList as ds}\n selectDatasource(ds)}\n class=\"w-full text-left px-3 py-2 text-sm hover:bg-blue-50 border-b border-gray-100 last:border-b-0\"\n >\n {ds.table_name}\n {ds.schema}\n {ds.database_name} · {ds.database_dialect}\n \n {/each}\n
\n {/if}\n
\n {#if databaseDialect}\n
\n \n {$t.translate?.config?.detected_dialect.replace('{dialect}', databaseDialect)}\n \n
\n {/if}\n
\n\n {#if isColumnsLoading}\n
\n {/if}\n\n {#if columnList.length > 0}\n
\n \n
\n {#each columnList as col}\n
\n {col.name}\n {#if col.type}\n ({col.type})\n {/if}\n {#if isVirtual(col.name)}\n {$t.translate?.config?.virtual}\n {/if}\n
\n {/each}\n
\n
\n {/if}\n
\n\n \n
\n

{$t.translate?.config?.column_mapping}

\n\n \n
\n \n \n \n {#each columnList as col}\n \n {/each}\n \n {#if validationErrors.translationColumn}\n

{validationErrors.translationColumn}

\n {/if}\n
\n\n \n
\n
\n \n {#if sourceKeyCols.length > 0}\n { sourceKeyCols = []; targetKeyCols = []; }}\n class=\"text-xs text-red-600 hover:text-red-700\"\n >\n {$t.translate?.config?.clear_all}\n \n {/if}\n
\n\n {#if columnList.length === 0}\n
\n {\n sourceKeyCols = [...sourceKeyCols, ''];\n targetKeyCols = [...targetKeyCols, ''];\n }}\n class=\"px-3 py-1 text-xs border border-dashed border-gray-300 rounded text-gray-500 hover:border-primary-ring hover:text-primary\"\n >\n {$t.translate?.config?.add_key_column}\n \n
\n {:else}\n
\n {#each columnList as col}\n toggleSourceKeyCol(col.name)}\n class=\"px-2.5 py-1 text-xs rounded-full border transition-colors\n {sourceKeyCols.includes(col.name)\n ? 'bg-blue-100 border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n title={isVirtual(col.name) ? $t.translate?.config?.virtual_column_warning.replace('{col}', col.name) : ''}\n >\n {col.name}\n {#if isVirtual(col.name)}\n *\n {/if}\n \n {/each}\n
\n {/if}\n\n {#if sourceKeyCols.length > 0}\n
\n {#each sourceKeyCols as srcCol, idx}\n
\n \n \n updateTargetKeyCol(idx, e.target.value)}\n class=\"flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm font-mono focus-visible:ring-2 focus-visible:ring-primary-ring\"\n />\n {\n sourceKeyCols = sourceKeyCols.filter((_, i) => i !== idx);\n targetKeyCols = targetKeyCols.filter((_, i) => i !== idx);\n }}\n class=\"p-1 text-gray-400 hover:text-red-500\"\n >\n \n \n \n \n
\n {/each}\n
\n {/if}\n {#if validationErrors.targetKeyCols}\n

{validationErrors.targetKeyCols}

\n {/if}\n
\n\n \n
\n \n {#if columnList.length === 0}\n

{$t.translate?.config?.select_datasource_hint}

\n {:else}\n
\n {#each columnList as col}\n toggleContextColumn(col.name)}\n class=\"px-2.5 py-1 text-xs rounded-full border transition-colors\n {contextColumns.includes(col.name)\n ? 'bg-purple-100 border-purple-300 text-purple-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {col.name}\n {#if isVirtual(col.name)}\n *\n {/if}\n \n {/each}\n
\n {/if}\n
\n
\n\n \n
\n

{$t.translate?.config?.llm_settings}

\n
\n
\n \n \n
\n
\n \n {#if targetLanguages.length === 0}\n

{$t.translate?.config?.target_language_required || 'Select at least one target language'}

\n {/if}\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n
\n \n
\n\n \n
\n \n
\n
\n\n \n
\n

{$t.translate?.config?.terminology_dictionaries}

\n {#if availableDictionaries.length === 0}\n

\n {$t.translate?.config?.no_dictionaries}\n

\n {:else}\n
\n {#each availableDictionaries as dict}\n \n {/each}\n
\n {/if}\n
\n
\n\n" + }, { "contract_id": "CorrectionCell", "contract_type": "Component", @@ -65965,6 +66995,24 @@ "tier_source": "AutoCalculated", "body": "\n\n\n\n\n\n\n\n\n{#if uxState === 'view'}\n\t e.key === 'Enter' && startEditing()}\n\t>\n\t\t\t{#if displayValue}\n\t\t\t{displayValue}\n\t\t{:else}\n\t\t\t\n\t\t{/if}\n\t\n\n\n{:else if uxState === 'editing'}\n\t
\n\t\t {\n\t\t\t\tif (e.key === 'Enter') saveEdit();\n\t\t\t\tif (e.key === 'Escape') cancelEditing();\n\t\t\t}}\n\t\t/>\n\t\t
\n\t\t\t\n\t\t\t\t✓\n\t\t\t\n\t\t\t\n\t\t\t\t✕\n\t\t\t\n\t\t
\n\t
\n\n\n{:else if uxState === 'saving'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_saving}\n\t
\n\n\n{:else if uxState === 'saved'}\n\t
\n\t\t{displayValue}\n\t\t📝\n\t\t\n\t\t\t📕 {$t.translate?.corrections?.cell_submit_to_dict}\n\t\t\n\t
\n\n\n{:else if uxState === 'submitting_to_dict'}\n\t
\n\t\t
\n\t\t{$t.translate?.corrections?.cell_submitting}\n\t
\n\n\n{:else if uxState === 'dict_submitted'}\n\t
\n\t\t{displayValue}\n\t\t✅ {$t.translate?.corrections?.cell_submitted_badge}\n\t
\n\n\n{:else if uxState === 'error'}\n\t
\n\t\t{displayValue}\n\t\t{errorMessage}\n\t\t\n\t\t\t{$t.translate?.corrections?.cell_retry}\n\t\t\n\t
\n{/if}\n\n\n{#if dictPopupOpen}\n\t\n\t\t
e.stopPropagation()}>\n\t\t\t

{$t.translate?.corrections?.cell_submit_to_dict}

\n\t\t\t
\n\t\t\t\t{#if sourceLanguage}\n\t\t\t\t\t
\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_language_pair} {sourceLanguage} → {languageCode}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
{editValue}
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{#each dictionaries as dict}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t\n\t\t\t\t{#if editableContextData || contextEditMode}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t{$t.translate?.corrections?.cell_context_data}\n\t\t\t\t\t\t\t { contextEditMode = !contextEditMode; if (!contextEditMode) editableContextData = contextData ? JSON.parse(JSON.stringify(contextData)) : null; }}\n\t\t\t\t\t\t\t\tclass=\"text-xs text-primary hover:text-blue-700\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{contextEditMode ? $t.translate?.corrections?.cell_cancel_edit : $t.translate?.corrections?.cell_edit}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t{#if contextEditMode}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t{:else if typeof editableContextData === 'object' && editableContextData !== null}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t{#each Object.entries(editableContextData) as [key, val]}\n\t\t\t\t\t\t\t\t\t
{key}: {String(val).substring(0, 100)}
\n\t\t\t\t\t\t\t\t{/each}\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t

{$t.translate?.corrections?.cell_no_context_data}

\n\t\t\t\t\t\t{/if}\n\t\t\t\t\t
\n\t\t\t\t{/if}\n\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_cancel}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{$t.translate?.corrections?.cell_submit}\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n{/if}\n\n" }, + { + "contract_id": "RunTabContent", + "contract_type": "Component", + "file_path": "frontend/src/lib/components/translate/RunTabContent.svelte", + "start_line": 1, + "end_line": 199, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Run tab — translation execution with incremental/full mode selection, progress", + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

{$t.translate?.config?.run_translation}

\n\n \n
\n {$t.translate?.config?.status}:\n \n {getJobStatusLabel(status)}\n \n {#if status === 'DRAFT'}\n \n {$t.translate?.config?.mark_ready || 'Mark as READY'}\n \n {/if}\n
\n\n
\n \n
\n \n
\n
\n
\n \n \n \n
\n
\n

{$t.translate?.config?.run_incremental}

\n

{$t.translate?.config?.run_incremental_desc}

\n onTriggerRun(false)}\n disabled={isRunning || status === 'DRAFT'}\n class=\"mt-3 px-5 py-1.5 text-sm font-medium bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isRunning && !isFullRun ? $t.translate?.config?.running : $t.translate?.config?.run_translation}\n \n
\n
\n
\n\n \n
\n
\n
\n \n \n \n
\n
\n

{$t.translate?.config?.run_full}

\n

{$t.translate?.config?.run_full_desc}

\n onTriggerRun(true)}\n disabled={isRunning || status === 'DRAFT'}\n class=\"mt-3 px-5 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isRunning && isFullRun ? $t.translate?.config?.running : $t.translate?.config?.full_translate}\n \n
\n
\n
\n
\n\n {#if runError}\n
\n

{runError}

\n
\n {/if}\n\n {#if currentRunId && !runComplete}\n \n {/if}\n\n {#if completedRuns.length > 0}\n
\n
\n

{$t.translate?.config?.recent_runs}

\n showPageBulkReplace = true}\n class=\"px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors\"\n >\n {$t.translate?.run?.bulk_replace || 'Bulk Replace'}\n \n
\n
\n {#each completedRuns as run}\n
\n
\n
\n
\n {$t.translate?.run?.run_id}\n {run.id}\n \n {run.status}\n \n
\n
\n {$t.translate?.run?.total}: {run.total_records || 0}\n {$t.translate?.run?.success}: {run.successful_records || 0}\n {$t.translate?.run?.failed}: {run.failed_records || 0}\n {$t.translate?.run?.skipped}: {run.skipped_records || 0}\n
\n {#if run.error_message}\n

{run.error_message}

\n {/if}\n
\n onToggleRunDetails(run.id)}\n class=\"shrink-0 px-3 py-1.5 text-xs border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors\"\n >\n {expandedRunIds.includes(run.id)\n ? ($t.translate?.run?.hide_details || 'Hide details')\n : ($t.translate?.run?.show_details || 'Show details')}\n \n
\n {#if expandedRunIds.includes(run.id)}\n
\n \n
\n {/if}\n
\n {/each}\n
\n
\n {/if}\n
\n
\n\n" + }, { "contract_id": "ScheduleConfig", "contract_type": "Component", @@ -66012,6 +67060,24 @@ "tier_source": "AutoCalculated", "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n
\n \n {#if state === 'checking'}\n \n \n \n \n {:else if state === 'complete' && result?.all_present}\n \n \n \n {:else if state === 'complete' && !result?.table_exists}\n \n \n \n {:else if state === 'complete' && !result?.all_present}\n \n \n \n {:else}\n \n \n \n {/if}\n\n \n {_('translate.target_schema.title') || 'Target Schema'}\n \n
\n\n \n \n {buttonLabel}\n \n
\n\n \n
\n {#if state === 'idle'}\n

\n {#if !targetTable}\n {_('translate.target_schema.enter_table') || 'Enter a target table name and click \"Check Target Schema\".'}\n {:else if !targetDatabaseId}\n {_('translate.target_schema.select_database') || 'Select a target database and click \"Check Target Schema\".'}\n {:else}\n {_('translate.target_schema.press_button') || 'Click \"Check Target Schema\" to verify the target table columns.'}\n {/if}\n

\n\n {:else if state === 'checking'}\n

\n {_('translate.target_schema.checking') || 'Checking target table schema…'}\n

\n\n {:else if state === 'complete' && result}\n {#key renderKey}\n {#if !result.table_exists}\n
\n \n \n \n

{_('translate.target_schema.table_not_found') || 'Target table not found in the database.'}

\n
\n\n {:else if result.all_present}\n
\n \n \n \n

{_('translate.target_schema.all_present') || 'Target table has all required columns.'}

\n
\n\n {:else}\n
\n \n \n \n

{_('translate.target_schema.missing_columns_title') || 'Target table is missing some required columns.'}

\n
\n {/if}\n\n \n {#if actualColumns.length > 0}\n
\n

\n {_('translate.target_schema.expected_columns') || 'Required columns for translation inserts:'}\n

\n
\n {#each actualColumns as col}\n \n {#if col.is_missing}\n \n \n \n {:else}\n \n \n \n {/if}\n {col.name}\n {#if col.data_type}({col.data_type}){/if}\n \n {/each}\n
\n
\n {/if}\n {/key}\n\n \n {#if result.table_exists && !result.all_present}\n
\n

{_('translate.target_schema.add_columns') || 'Add these missing columns to the target table, or configure custom column names in \"Target Column Mapping\" above.'}

\n
\n {/if}\n\n \n {#if result.error}\n

{result.error}

\n {/if}\n\n {:else if state === 'error'}\n
\n \n \n \n

\n {_('translate.target_schema.check_error') || 'Could not verify target table schema. Make sure the table exists and is accessible.'}\n

\n
\n {/if}\n
\n
\n\n" }, + { + "contract_id": "TargetTabForm", + "contract_type": "Component", + "file_path": "frontend/src/lib/components/translate/TargetTabForm.svelte", + "start_line": 1, + "end_line": 151, + "tier": "TIER_2", + "complexity": 3, + "metadata": { + "BRIEF": "Target configuration tab — target schema/table/database selection, target column", + "COMPLEXITY": 3 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

{$t.translate?.config?.target_table_title}

\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n \n {#each databases as db}\n \n {/each}\n \n {#if databasesLoading}\n

{$t.translate?.config?.loading_databases}

\n {:else if !environmentId}\n

{$t.translate?.config?.select_environment}

\n {:else}\n

{$t.translate?.config?.target_database_hint}

\n {/if}\n
\n
\n\n \n
\n

{$t.translate?.config?.target_column_mapping_title || 'Target Column Mapping'}

\n

{$t.translate?.config?.target_column_mapping_description || 'Configure which columns receive translated data, language codes, source text, and detected source language during INSERT.'}

\n
\n
\n \n \n

{$t.translate?.config?.target_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_language_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_source_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_source_language_column_hint}

\n
\n
\n
\n\n \n \n
\n\n" + }, { "contract_id": "TermCorrectionPopup", "contract_type": "Component", @@ -68806,27 +69872,25 @@ "contract_type": "Store", "file_path": "frontend/src/lib/stores/translationRun.js", "start_line": 1, - "end_line": 198, + "end_line": 279, "tier": "TIER_2", - "complexity": 3, + "complexity": 4, "metadata": { - "BRIEF": "Global store for active translation run progress — survives page navigation.", - "COMPLEXITY": 3, - "PROPERTY": "{boolean} isActive - Derived: true when polling is active", - "TYPEDEF": "{Object} TranslationRunState" + "BRIEF": "Global store for active translation run progress — uses WebSocket for real-time", + "COMPLEXITY": 4 }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region TranslationRunStore [C:3] [TYPE Store] [SEMANTICS translate, run, progress, polling, store]\n// @BRIEF Global store for active translation run progress — survives page navigation.\n// @TYPEDEF {'idle'|'running'|'inserting'|'completed'|'partial'|'failed'|'insert_failed'|'cancelled'} UxState\n\n\n/**\n * @typedef {Object} TranslationRunState\n * @property {string|null} runId - Active translation run ID\n * @property {UxState} uxState - Current UX state\n * @property {Object|null} status - Raw status data from API\n * @property {number} totalRecords\n * @property {number} successfulRecords\n * @property {number} failedRecords\n * @property {number} skippedRecords\n * @property {number} progressPct - 0-100\n * @property {string|null} insertStatus\n * @property {number} batchCount\n * @property {string|null} jobId - The job this run belongs to (for navigation)\n * @property {boolean} isFullRun\n * @property {boolean} isActive - Derived: true when polling is active\n */\n\nimport { writable, derived } from 'svelte/store';\nimport { fetchRunStatus } from '$lib/api/translate.js';\n\nconst initialState = {\n\trunId: null,\n\tuxState: 'idle',\n\tstatus: null,\n\ttotalRecords: 0,\n\tsuccessfulRecords: 0,\n\tfailedRecords: 0,\n\tskippedRecords: 0,\n\tprogressPct: 0,\n\tinsertStatus: null,\n\tbatchCount: 0,\n\tjobId: null,\n\tisFullRun: false,\n};\n\n/** @type {import('svelte/store').Writable} */\nexport const translationRunStore = writable(initialState);\n\n/** Derived: true when a run is actively being polled */\nexport const isTranslationActive = derived(\n\ttranslationRunStore,\n\t($store) => $store.uxState === 'running' || $store.uxState === 'inserting'\n);\n\n/** Derived: true when run has finished */\nexport const isTranslationFinished = derived(\n\ttranslationRunStore,\n\t($store) =>\n\t\t$store.uxState === 'completed' ||\n\t\t$store.uxState === 'partial' ||\n\t\t$store.uxState === 'failed' ||\n\t\t$store.uxState === 'insert_failed' ||\n\t\t$store.uxState === 'cancelled'\n);\n\n// Internal polling state (not reactive)\nlet _pollingInterval = null;\nlet _pollCount = 0;\nconst MAX_POLLS = 300;\nlet _onCompleteCallback = null;\n\n/**\n * Clear the onComplete callback without stopping polling.\n * Used by page components when they unmount — the store keeps polling\n * so the global indicator still works, but the page callback is detached.\n */\nexport function clearOnCompleteCallback() {\n\t_onCompleteCallback = null;\n}\n\n/**\n * Start polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */\nexport function startTranslationRun(runId, options = {}) {\n\tif (!runId) return;\n\n\tstopTranslationRun();\n\n\ttranslationRunStore.set({\n\t\t...initialState,\n\t\trunId,\n\t\tuxState: 'running',\n\t\tjobId: options.jobId || null,\n\t\tisFullRun: options.isFullRun || false,\n\t});\n\n\t_onCompleteCallback = options.onComplete || null;\n\t_pollCount = 0;\n\t_pollingInterval = setInterval(pollStatus, 2000);\n\tpollStatus();\n}\n\n/**\n * Stop polling without clearing state (run may persist).\n */\nexport function stopTranslationRun() {\n\tif (_pollingInterval) {\n\t\tclearInterval(_pollingInterval);\n\t\t_pollingInterval = null;\n\t}\n}\n\n/**\n * Reset store to idle.\n */\nexport function resetTranslationRun() {\n\tstopTranslationRun();\n\ttranslationRunStore.set(initialState);\n\t_onCompleteCallback = null;\n}\n\n/**\n * Manually set the run state (used by page component for lifecycle hooks).\n * @param {Partial} state\n */\nexport function updateTranslationRunState(state) {\n\ttranslationRunStore.update(prev => ({ ...prev, ...state }));\n}\n\n/** @returns {Promise} */\nasync function pollStatus() {\n\t_pollCount++;\n\tif (_pollCount > MAX_POLLS) {\n\t\tconsole.warn('[translationRunStore] Max polls reached, stopping');\n\t\tstopTranslationRun();\n\t\ttranslationRunStore.update(s => ({ ...s, uxState: 'failed' }));\n\t\tif (_onCompleteCallback) _onCompleteCallback({ status: 'TIMEOUT', error_message: 'Translation run timed out' });\n\t\treturn;\n\t}\n\n\tlet currentState;\n\ttranslationRunStore.subscribe(s => { currentState = s; })();\n\tif (!currentState || !currentState.runId) {\n\t\tstopTranslationRun();\n\t\treturn;\n\t}\n\n\ttry {\n\t\tconst data = await fetchRunStatus(currentState.runId);\n\t\tif (!data) return;\n\n\t\tconst s = data?.status;\n\t\tconst insertS = data?.insert_status;\n\n\t\tlet newUxState = currentState.uxState;\n\n\t\tif (s === 'PENDING' || s === 'RUNNING') {\n\t\t\tnewUxState = (insertS === 'started' || insertS === 'pending' || insertS === 'running')\n\t\t\t\t? 'inserting' : 'running';\n\t\t} else if (s === 'COMPLETED') {\n\t\t\tnewUxState = (insertS === 'failed' || insertS === 'timeout') ? 'insert_failed'\n\t\t\t\t: (data.failed_records > 0) ? 'partial' : 'completed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'FAILED') {\n\t\t\tnewUxState = 'failed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'CANCELLED') {\n\t\t\tnewUxState = 'cancelled';\n\t\t\tstopTranslationRun();\n\t\t}\n\n\t\tconst total = data?.total_records || 0;\n\t\tconst pct = total > 0\n\t\t\t? Math.round(((data.successful_records + data.failed_records + data.skipped_records) / total) * 100)\n\t\t\t: 0;\n\n\t\ttranslationRunStore.update(prev => ({\n\t\t\t...prev,\n\t\t\tuxState: newUxState,\n\t\t\tstatus: data,\n\t\t\ttotalRecords: total,\n\t\t\tsuccessfulRecords: data?.successful_records || 0,\n\t\t\tfailedRecords: data?.failed_records || 0,\n\t\t\tskippedRecords: data?.skipped_records || 0,\n\t\t\tprogressPct: pct,\n\t\t\tinsertStatus: data?.insert_status || null,\n\t\t\tbatchCount: data?.batch_count || 0,\n\t\t}));\n\n\t\t// Fire completion callback if terminal\n\t\tif (newUxState !== 'running' && newUxState !== 'inserting') {\n\t\t\tif (_onCompleteCallback) _onCompleteCallback(data);\n\t\t}\n\t} catch (err) {\n\t\tconsole.warn('[translationRunStore] Poll error:', err);\n\t}\n}\n// #endregion TranslationRunStore\n" + "body": "// #region TranslationRunStore [C:4] [TYPE Store] [SEMANTICS translate, run, progress, websocket, polling, store]\n// @BRIEF Global store for active translation run progress — uses WebSocket for real-time\n// progress + polling fallback for structured status. Survives page navigation.\n// @RELATION DEPENDS_ON -> [EXT:frontend:fetchRunStatus]\n// @RELATION DEPENDS_ON -> [EXT:frontend:getWsUrl]\n// @UX_STATE idle -> No active run\n// @UX_STATE running -> Polling + WebSocket active, progress bar updates in real-time\n// @UX_STATE completed/partial/failed/insert_failed/cancelled -> Terminal states\n// @UX_FEEDBACK WebSocket provides real-time batch completion events during translate phase\n// @UX_FEEDBACK Polling provides structured status updates (insert phase, final counts)\n// @UX_RECOVERY WebSocket disconnect -> continues with polling only\n// @TYPEDEF {'idle'|'running'|'inserting'|'completed'|'partial'|'failed'|'insert_failed'|'cancelled'} UxState\n\n\n/**\n * @typedef {Object} TranslationRunState\n * @property {string|null} runId - Active translation run ID\n * @property {UxState} uxState - Current UX state\n * @property {Object|null} status - Raw status data from API\n * @property {number} totalRecords\n * @property {number} successfulRecords\n * @property {number} failedRecords\n * @property {number} skippedRecords\n * @property {number} progressPct - 0-100\n * @property {string|null} insertStatus\n * @property {number} batchCount\n * @property {string|null} jobId - The job this run belongs to (for navigation)\n * @property {boolean} isFullRun\n * @property {boolean} isActive - Derived: true when polling is active\n */\n\nimport { writable, derived } from 'svelte/store';\nimport { fetchRunStatus } from '$lib/api/translate.js';\nimport { getTranslateRunWsUrl } from '$lib/api.js';\n\nconst initialState = {\n\trunId: null,\n\tuxState: 'idle',\n\tstatus: null,\n\ttotalRecords: 0,\n\tsuccessfulRecords: 0,\n\tfailedRecords: 0,\n\tskippedRecords: 0,\n\tprogressPct: 0,\n\tinsertStatus: null,\n\tbatchCount: 0,\n\tjobId: null,\n\tisFullRun: false,\n};\n\n/** @type {import('svelte/store').Writable} */\nexport const translationRunStore = writable(initialState);\n\n/** Derived: true when a run is actively being polled */\nexport const isTranslationActive = derived(\n\ttranslationRunStore,\n\t($store) => $store.uxState === 'running' || $store.uxState === 'inserting'\n);\n\n/** Derived: true when run has finished */\nexport const isTranslationFinished = derived(\n\ttranslationRunStore,\n\t($store) =>\n\t\t$store.uxState === 'completed' ||\n\t\t$store.uxState === 'partial' ||\n\t\t$store.uxState === 'failed' ||\n\t\t$store.uxState === 'insert_failed' ||\n\t\t$store.uxState === 'cancelled'\n);\n\n// Internal polling + WebSocket state (not reactive)\nlet _pollingInterval = null;\nlet _pollCount = 0;\nconst MAX_POLLS = 300;\nlet _onCompleteCallback = null;\nlet _ws = null;\n\n/**\n * Clear the onComplete callback without stopping polling.\n * Used by page components when they unmount — the store keeps polling\n * so the global indicator still works, but the page callback is detached.\n */\nexport function clearOnCompleteCallback() {\n\t_onCompleteCallback = null;\n}\n\n/**\n * Start WebSocket + polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */\nexport function startTranslationRun(runId, options = {}) {\n\tif (!runId) return;\n\n\tstopTranslationRun();\n\n\ttranslationRunStore.set({\n\t\t...initialState,\n\t\trunId,\n\t\tuxState: 'running',\n\t\tjobId: options.jobId || null,\n\t\tisFullRun: options.isFullRun || false,\n\t});\n\n\t_onCompleteCallback = options.onComplete || null;\n\n\t// WebSocket for real-time progress events (task log stream)\n\t_connectWebSocket(runId);\n\n\t// Polling for structured status data (fallback + insert phase)\n\t_pollCount = 0;\n\t_pollingInterval = setInterval(pollStatus, 2000);\n\tpollStatus();\n}\n\n/**\n * Connect WebSocket to /ws/translate/run/{runId} for real-time progress events.\n * Receives structured run status JSON each second.\n * Falls back silently to polling-only if WebSocket fails.\n * @param {string} runId\n */\nfunction _connectWebSocket(runId) {\n\ttry {\n\t\tconst wsUrl = getTranslateRunWsUrl(runId);\n\t\t_ws = new WebSocket(wsUrl);\n\n\t\t_ws.onmessage = (event) => {\n\t\t\ttry {\n\t\t\t\tconst data = JSON.parse(event.data);\n\t\t\t\tif (data.error) return;\n\t\t\t\tconst s = data.status;\n\t\t\t\tconst total = data.total_records || 0;\n\t\t\t\tconst done = (data.successful_records || 0) + (data.failed_records || 0) + (data.skipped_records || 0);\n\t\t\t\tconst pct = total > 0 ? Math.round((done / total) * 100) : 0;\n\t\t\t\tconst newUxState = s === 'PENDING' || s === 'RUNNING'\n\t\t\t\t\t? (data.insert_status === 'started' || data.insert_status === 'pending' || data.insert_status === 'running' ? 'inserting' : 'running')\n\t\t\t\t\t: s === 'COMPLETED'\n\t\t\t\t\t\t? (data.insert_status === 'failed' || data.insert_status === 'timeout' ? 'insert_failed' : (data.failed_records > 0 ? 'partial' : 'completed'))\n\t\t\t\t\t\t: s === 'FAILED' ? 'failed'\n\t\t\t\t\t\t: s === 'CANCELLED' ? 'cancelled'\n\t\t\t\t\t\t: null;\n\t\t\t\tif (newUxState) {\n\t\t\t\t\ttranslationRunStore.update(prev => ({\n\t\t\t\t\t\t...prev,\n\t\t\t\t\t\tuxState: newUxState,\n\t\t\t\t\t\tstatus: data,\n\t\t\t\t\t\ttotalRecords: total,\n\t\t\t\t\t\tsuccessfulRecords: data.successful_records || 0,\n\t\t\t\t\t\tfailedRecords: data.failed_records || 0,\n\t\t\t\t\t\tskippedRecords: data.skipped_records || 0,\n\t\t\t\t\t\tprogressPct: pct,\n\t\t\t\t\t\tinsertStatus: data.insert_status || null,\n\t\t\t\t\t\tbatchCount: data.batch_count || 0,\n\t\t\t\t\t}));\n\t\t\t\t\tif (newUxState !== 'running' && newUxState !== 'inserting') {\n\t\t\t\t\t\tif (_onCompleteCallback) _onCompleteCallback(data);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (_e) {\n\t\t\t\t// Ignore parse errors\n\t\t\t}\n\t\t};\n\n\t\t_ws.onerror = () => {\n\t\t\t_ws = null;\n\t\t};\n\n\t\t_ws.onclose = () => {\n\t\t\t_ws = null;\n\t\t};\n\t} catch (_e) {\n\t\t_ws = null;\n\t}\n}\n\n/**\n * Stop WebSocket + polling without clearing state (run may persist).\n */\nexport function stopTranslationRun() {\n\tif (_ws) {\n\t\t_ws.onclose = null; // prevent reconnect\n\t\t_ws.close();\n\t\t_ws = null;\n\t}\n\tif (_pollingInterval) {\n\t\tclearInterval(_pollingInterval);\n\t\t_pollingInterval = null;\n\t}\n}\n\n/**\n * Reset store to idle.\n */\nexport function resetTranslationRun() {\n\tstopTranslationRun();\n\ttranslationRunStore.set(initialState);\n\t_onCompleteCallback = null;\n}\n\n/**\n * Manually set the run state (used by page component for lifecycle hooks).\n * @param {Partial} state\n */\nexport function updateTranslationRunState(state) {\n\ttranslationRunStore.update(prev => ({ ...prev, ...state }));\n}\n\n/** @returns {Promise} */\nasync function pollStatus() {\n\t_pollCount++;\n\tif (_pollCount > MAX_POLLS) {\n\t\tconsole.warn('[translationRunStore] Max polls reached, stopping');\n\t\tstopTranslationRun();\n\t\ttranslationRunStore.update(s => ({ ...s, uxState: 'failed' }));\n\t\tif (_onCompleteCallback) _onCompleteCallback({ status: 'TIMEOUT', error_message: 'Translation run timed out' });\n\t\treturn;\n\t}\n\n\tlet currentState;\n\ttranslationRunStore.subscribe(s => { currentState = s; })();\n\tif (!currentState || !currentState.runId) {\n\t\tstopTranslationRun();\n\t\treturn;\n\t}\n\n\ttry {\n\t\tconst data = await fetchRunStatus(currentState.runId);\n\t\tif (!data) return;\n\n\t\tconst s = data?.status;\n\t\tconst insertS = data?.insert_status;\n\n\t\tlet newUxState = currentState.uxState;\n\n\t\tif (s === 'PENDING' || s === 'RUNNING') {\n\t\t\tnewUxState = (insertS === 'started' || insertS === 'pending' || insertS === 'running')\n\t\t\t\t? 'inserting' : 'running';\n\t\t} else if (s === 'COMPLETED') {\n\t\t\tnewUxState = (insertS === 'failed' || insertS === 'timeout') ? 'insert_failed'\n\t\t\t\t: (data.failed_records > 0) ? 'partial' : 'completed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'FAILED') {\n\t\t\tnewUxState = 'failed';\n\t\t\tstopTranslationRun();\n\t\t} else if (s === 'CANCELLED') {\n\t\t\tnewUxState = 'cancelled';\n\t\t\tstopTranslationRun();\n\t\t}\n\n\t\tconst total = data?.total_records || 0;\n\t\tconst pct = total > 0\n\t\t\t? Math.round(((data.successful_records + data.failed_records + data.skipped_records) / total) * 100)\n\t\t\t: 0;\n\n\t\ttranslationRunStore.update(prev => ({\n\t\t\t...prev,\n\t\t\tuxState: newUxState,\n\t\t\tstatus: data,\n\t\t\ttotalRecords: total,\n\t\t\tsuccessfulRecords: data?.successful_records || 0,\n\t\t\tfailedRecords: data?.failed_records || 0,\n\t\t\tskippedRecords: data?.skipped_records || 0,\n\t\t\tprogressPct: pct,\n\t\t\tinsertStatus: data?.insert_status || null,\n\t\t\tbatchCount: data?.batch_count || 0,\n\t\t}));\n\n\t\t// Fire completion callback if terminal\n\t\tif (newUxState !== 'running' && newUxState !== 'inserting') {\n\t\t\tif (_onCompleteCallback) _onCompleteCallback(data);\n\t\t}\n\t} catch (err) {\n\t\tconsole.warn('[translationRunStore] Poll error:', err);\n\t}\n}\n// #endregion TranslationRunStore\n" }, { "contract_id": "frontend/src/lib/stores/translationRun.js::clearOnCompleteCallback", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 67, - "end_line": 72, + "start_line": 78, + "end_line": 83, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68840,8 +69904,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::startTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 76, - "end_line": 84, + "start_line": 87, + "end_line": 95, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68849,14 +69913,29 @@ "schema_warnings": [], "anchor_syntax": "jsdoc", "tier_source": "AutoCalculated", - "body": "/**\n * Start polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */" + "body": "/**\n * Start WebSocket + polling for a translation run.\n * @param {string} runId\n * @param {Object} [options]\n * @param {string} [options.jobId] - Job ID for navigation\n * @param {boolean} [options.isFullRun]\n * @param {Function} [options.onComplete] - Called when run reaches terminal state\n */" + }, + { + "contract_id": "frontend/src/lib/stores/translationRun.js::_connectWebSocket", + "contract_type": "Function", + "file_path": "frontend/src/lib/stores/translationRun.js", + "start_line": 119, + "end_line": 125, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "jsdoc", + "tier_source": "AutoCalculated", + "body": "/**\n * Connect WebSocket to /ws/translate/run/{runId} for real-time progress events.\n * Receives structured run status JSON each second.\n * Falls back silently to polling-only if WebSocket fails.\n * @param {string} runId\n */" }, { "contract_id": "frontend/src/lib/stores/translationRun.js::stopTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 103, - "end_line": 106, + "start_line": 179, + "end_line": 182, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68864,14 +69943,14 @@ "schema_warnings": [], "anchor_syntax": "jsdoc", "tier_source": "AutoCalculated", - "body": "/**\n * Stop polling without clearing state (run may persist).\n */" + "body": "/**\n * Stop WebSocket + polling without clearing state (run may persist).\n */" }, { "contract_id": "frontend/src/lib/stores/translationRun.js::resetTranslationRun", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 113, - "end_line": 116, + "start_line": 194, + "end_line": 197, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68885,8 +69964,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::updateTranslationRunState", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 122, - "end_line": 126, + "start_line": 203, + "end_line": 207, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68900,8 +69979,8 @@ "contract_id": "frontend/src/lib/stores/translationRun.js::pollStatus", "contract_type": "Function", "file_path": "frontend/src/lib/stores/translationRun.js", - "start_line": 130, - "end_line": 131, + "start_line": 211, + "end_line": 212, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -68916,7 +69995,7 @@ "contract_type": "Module", "file_path": "frontend/src/lib/toasts.js", "start_line": 1, - "end_line": 65, + "end_line": 68, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -68934,14 +70013,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region ToastsModule [C:3] [TYPE Module] [SEMANTICS toast, notification, store, dedup, feedback]\n// @BRIEF Manages toast notifications using a Svelte writable store with deduplication and auto-removal.\n// @RELATION BINDS_TO -> [ApiModule]\n\n// #region toasts_module:Module [TYPE Function]\n// @SEMANTICS: notification, toast, feedback, state\n// @PURPOSE: Manages toast notifications using a Svelte writable store.\n// @LAYER UI\n\nimport { writable } from 'svelte/store';\n\n// #region toasts:Data [TYPE Function]\n// @PURPOSE: Writable store containing the list of active toasts.\nexport const toasts = writable([]);\n// #endregion toasts:Data\n\nconst TOAST_DEDUP_WINDOW_MS = 1200;\nconst recentToastByKey = new Map();\n\nfunction buildToastKey(message, type) {\n return `${String(type || 'info')}::${String(message || '')}`;\n}\n\nfunction shouldSkipDuplicateToast(message, type) {\n const key = buildToastKey(message, type);\n const now = Date.now();\n const previousAt = recentToastByKey.get(key);\n if (previousAt && now - previousAt < TOAST_DEDUP_WINDOW_MS) {\n return true;\n }\n recentToastByKey.set(key, now);\n return false;\n}\n\n// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error).\n// @PARAM: duration (number) - Duration in ms before the toast is removed.\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}'}}`);\n toasts.update(all => [...all, { id, message, type }]);\n setTimeout(() => removeToast(id), duration);\n}\n// #endregion addToast:Function\n\n// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nfunction removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n// #endregion toasts_module:Module\n// #endregion ToastsModule\n" + "body": "// #region ToastsModule [C:3] [TYPE Module] [SEMANTICS toast, notification, store, dedup, feedback]\n// @BRIEF Manages toast notifications using a Svelte writable store with deduplication and auto-removal.\n// @RELATION BINDS_TO -> [ApiModule]\n\n// #region toasts_module:Module [TYPE Function]\n// @SEMANTICS: notification, toast, feedback, state\n// @PURPOSE: Manages toast notifications using a Svelte writable store.\n// @LAYER UI\n\nimport { writable } from 'svelte/store';\n\n// #region toasts:Data [TYPE Function]\n// @PURPOSE: Writable store containing the list of active toasts.\nexport const toasts = writable([]);\n// #endregion toasts:Data\n\nconst TOAST_DEDUP_WINDOW_MS = 1200;\nconst recentToastByKey = new Map();\n\nfunction buildToastKey(message, type) {\n return `${String(type || 'info')}::${String(message || '')}`;\n}\n\nfunction shouldSkipDuplicateToast(message, type) {\n const key = buildToastKey(message, type);\n const now = Date.now();\n const previousAt = recentToastByKey.get(key);\n if (previousAt && now - previousAt < TOAST_DEDUP_WINDOW_MS) {\n return true;\n }\n recentToastByKey.set(key, now);\n return false;\n}\n\n// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error, warning).\n// @PARAM: duration (number) - Duration in ms before the toast is removed. 0 = persistent (must be dismissed manually).\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n const persistent = duration === 0;\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}', 'persistent': '${persistent}'}}`);\n toasts.update(all => [...all, { id, message, type, persistent }]);\n if (!persistent) {\n setTimeout(() => removeToast(id), duration);\n }\n}\n// #endregion addToast:Function\n\n// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nexport function removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n// #endregion toasts_module:Module\n// #endregion ToastsModule\n" }, { "contract_id": "toasts_module:Module", "contract_type": "Function", "file_path": "frontend/src/lib/toasts.js", "start_line": 5, - "end_line": 64, + "end_line": 67, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -68958,7 +70037,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region toasts_module:Module [TYPE Function]\n// @SEMANTICS: notification, toast, feedback, state\n// @PURPOSE: Manages toast notifications using a Svelte writable store.\n// @LAYER UI\n\nimport { writable } from 'svelte/store';\n\n// #region toasts:Data [TYPE Function]\n// @PURPOSE: Writable store containing the list of active toasts.\nexport const toasts = writable([]);\n// #endregion toasts:Data\n\nconst TOAST_DEDUP_WINDOW_MS = 1200;\nconst recentToastByKey = new Map();\n\nfunction buildToastKey(message, type) {\n return `${String(type || 'info')}::${String(message || '')}`;\n}\n\nfunction shouldSkipDuplicateToast(message, type) {\n const key = buildToastKey(message, type);\n const now = Date.now();\n const previousAt = recentToastByKey.get(key);\n if (previousAt && now - previousAt < TOAST_DEDUP_WINDOW_MS) {\n return true;\n }\n recentToastByKey.set(key, now);\n return false;\n}\n\n// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error).\n// @PARAM: duration (number) - Duration in ms before the toast is removed.\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}'}}`);\n toasts.update(all => [...all, { id, message, type }]);\n setTimeout(() => removeToast(id), duration);\n}\n// #endregion addToast:Function\n\n// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nfunction removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n// #endregion toasts_module:Module\n" + "body": "// #region toasts_module:Module [TYPE Function]\n// @SEMANTICS: notification, toast, feedback, state\n// @PURPOSE: Manages toast notifications using a Svelte writable store.\n// @LAYER UI\n\nimport { writable } from 'svelte/store';\n\n// #region toasts:Data [TYPE Function]\n// @PURPOSE: Writable store containing the list of active toasts.\nexport const toasts = writable([]);\n// #endregion toasts:Data\n\nconst TOAST_DEDUP_WINDOW_MS = 1200;\nconst recentToastByKey = new Map();\n\nfunction buildToastKey(message, type) {\n return `${String(type || 'info')}::${String(message || '')}`;\n}\n\nfunction shouldSkipDuplicateToast(message, type) {\n const key = buildToastKey(message, type);\n const now = Date.now();\n const previousAt = recentToastByKey.get(key);\n if (previousAt && now - previousAt < TOAST_DEDUP_WINDOW_MS) {\n return true;\n }\n recentToastByKey.set(key, now);\n return false;\n}\n\n// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error, warning).\n// @PARAM: duration (number) - Duration in ms before the toast is removed. 0 = persistent (must be dismissed manually).\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n const persistent = duration === 0;\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}', 'persistent': '${persistent}'}}`);\n toasts.update(all => [...all, { id, message, type, persistent }]);\n if (!persistent) {\n setTimeout(() => removeToast(id), duration);\n }\n}\n// #endregion addToast:Function\n\n// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nexport function removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n// #endregion toasts_module:Module\n" }, { "contract_id": "toasts:Data", @@ -68982,12 +70061,12 @@ "contract_type": "Function", "file_path": "frontend/src/lib/toasts.js", "start_line": 35, - "end_line": 52, + "end_line": 55, "tier": "TIER_1", "complexity": 1, "metadata": { "BRIEF": "Adds a new toast message.", - "PARAM": "duration (number) - Duration in ms before the toast is removed.", + "PARAM": "duration (number) - Duration in ms before the toast is removed. 0 = persistent (must be dismissed manually).", "POST": "New toast is added to the store and scheduled for removal.", "PRE": "message string is provided." }, @@ -68995,14 +70074,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error).\n// @PARAM: duration (number) - Duration in ms before the toast is removed.\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}'}}`);\n toasts.update(all => [...all, { id, message, type }]);\n setTimeout(() => removeToast(id), duration);\n}\n// #endregion addToast:Function\n" + "body": "// #region addToast:Function [TYPE Function]\n// @PURPOSE: Adds a new toast message.\n// @PRE: message string is provided.\n// @POST: New toast is added to the store and scheduled for removal.\n// @PARAM: message (string) - The message text.\n// @PARAM: type (string) - The type of toast (info, success, error, warning).\n// @PARAM: duration (number) - Duration in ms before the toast is removed. 0 = persistent (must be dismissed manually).\nexport function addToast(message, type = 'info', duration = 3000) {\n if (shouldSkipDuplicateToast(message, type)) {\n console.log(`[toasts.addToast][Action] Duplicate skipped context={{'type': '${type}', 'message': '${message}'}}`);\n return;\n }\n const id = Math.random().toString(36).substr(2, 9);\n const persistent = duration === 0;\n console.log(`[toasts.addToast][Action] Adding toast context={{'id': '${id}', 'type': '${type}', 'message': '${message}', 'persistent': '${persistent}'}}`);\n toasts.update(all => [...all, { id, message, type, persistent }]);\n if (!persistent) {\n setTimeout(() => removeToast(id), duration);\n }\n}\n// #endregion addToast:Function\n" }, { "contract_id": "removeToast:Function", "contract_type": "Function", "file_path": "frontend/src/lib/toasts.js", - "start_line": 54, - "end_line": 63, + "start_line": 57, + "end_line": 66, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -69015,7 +70094,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nfunction removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n" + "body": "// #region removeToast:Function [TYPE Function]\n// @PURPOSE: Removes a toast message by ID.\n// @PRE: id is provided.\n// @POST: Toast is removed from the store.\n// @PARAM: id (string) - The ID of the toast to remove.\nexport function removeToast(id) {\n console.log(`[toasts.removeToast][Action] Removing toast context={{'id': '${id}'}}`);\n toasts.update(all => all.filter(t => t.id !== id));\n}\n// #endregion removeToast:Function\n" }, { "contract_id": "Button", @@ -69248,7 +70327,7 @@ "contract_type": "Function", "file_path": "frontend/src/lib/ui/index.ts", "start_line": 1, - "end_line": 18, + "end_line": 20, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -69266,7 +70345,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region ui:Module [TYPE Function]\n//\n// @SEMANTICS: ui, components, library, atomic-design\n// @PURPOSE: Central export point for standardized UI components.\n// @LAYER: Atom\n//\n// @INVARIANT: All components exported here must follow Semantic Protocol.\n\n// [SECTION: EXPORTS]\nexport { default as Button } from './Button.svelte';\nexport { default as Input } from './Input.svelte';\nexport { default as Select } from './Select.svelte';\nexport { default as Card } from './Card.svelte';\nexport { default as PageHeader } from './PageHeader.svelte';\nexport { default as LanguageSwitcher } from './LanguageSwitcher.svelte';\n// [/SECTION: EXPORTS]\n\n// #endregion ui:Module\n" + "body": "// #region ui:Module [TYPE Function]\n//\n// @SEMANTICS: ui, components, library, atomic-design\n// @PURPOSE: Central export point for standardized UI components.\n// @LAYER: Atom\n//\n// @INVARIANT: All components exported here must follow Semantic Protocol.\n\n// [SECTION: EXPORTS]\nexport { default as Button } from './Button.svelte';\nexport { default as Input } from './Input.svelte';\nexport { default as Select } from './Select.svelte';\nexport { default as Card } from './Card.svelte';\nexport { default as PageHeader } from './PageHeader.svelte';\nexport { default as LanguageSwitcher } from './LanguageSwitcher.svelte';\nexport { default as Icon } from './Icon.svelte';\nexport { default as HelpTooltip } from './HelpTooltip.svelte';\n// [/SECTION: EXPORTS]\n\n// #endregion ui:Module\n" }, { "contract_id": "UtilsModule", @@ -69568,7 +70647,7 @@ "contract_type": "Layout", "file_path": "frontend/src/routes/+layout.svelte", "start_line": 1, - "end_line": 101, + "end_line": 112, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -69650,7 +70729,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\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\n\n\n
\n {#if isLoginPage}\n
\n {@render children?.()}\n
\n {:else}\n \n \n \n\n \n
\n \n \n \n
\n \n
\n\n {#if isProductionContext}\n
\n {$t.common?.prod_context_warning || \"PROD context active\"}:\n {selectedEnvironment?.name || selectedEnvironment?.id || 'environment'}\n
\n {/if}\n\n \n
\n {@render children?.()}\n
\n\n \n
\n
\n\n \n \n \n
\n {/if}\n
\n\n" + "body": "\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\n\n\n
\n {#if isLoginPage}\n
\n {@render children?.()}\n
\n {:else}\n \n \n \n\n \n
\n \n \n \n
\n \n
\n\n {#if isProductionContext}\n
\n
\n
\n \n
\n
\n {$t.common?.prod_context_warning || \"PROD context active\"}:\n {selectedEnvironment?.name || selectedEnvironment?.id || 'environment'}\n
\n
\n \n PROD\n
\n
\n
\n {/if}\n\n \n
\n {@render children?.()}\n
\n\n \n
\n
\n\n \n \n \n
\n {/if}\n
\n\n" }, { "contract_id": "RootLayoutConfig:Module", @@ -70494,7 +71573,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n
\n
\n \n \n {$t.common?.back}\n \n\n
\n

\n {dashboard?.title || $t.dashboard?.overview}\n

\n {#if hasGitRepo && gitDashboardRef}\n
\n \n
\n {/if}\n
\n\n

\n {$t.common?.id}: {resolvedDashboardId ?? dashboardRef}{#if dashboard?.slug} • {dashboard.slug}{/if}\n |\n \n \n Git: {gitMeta.label}\n {#if gitStatus?.ahead_count > 0}\n ({gitStatus.ahead_count})\n {/if}\n \n

\n
\n\n
\n (showGitManager = true)}\n >\n {$t.git?.management || \"Manage Git\"}\n \n \n {isStartingBackup ? $t.common?.loading || \"Loading...\" : $t.dashboard?.run_backup || \"Run backup\"}\n \n \n {isStartingValidation ? $t.common?.loading || \"Loading...\" : $t.dashboard?.run_llm_check || \"Run LLM check\"}\n \n \n {$t.common?.refresh}\n \n
\n
\n\n" + "body": "\n\n\n\n\n\n\n\n
\n
\n \n \n {$t.common?.back}\n \n\n
\n

\n {dashboard?.title || $t.dashboard?.overview}\n

\n {#if hasGitRepo && gitDashboardRef}\n
\n \n
\n {/if}\n
\n\n

\n {$t.common?.id}: {resolvedDashboardId ?? dashboardRef}{#if dashboard?.slug} • {dashboard.slug}{/if}\n |\n \n \n Git: {gitMeta.label}\n {#if gitStatus?.ahead_count > 0}\n ({gitStatus.ahead_count})\n {/if}\n \n

\n
\n\n
\n (showGitManager = true)}\n >\n {$t.git?.management}\n \n \n {isStartingBackup ? $t.common?.loading : $t.dashboard?.run_backup}\n \n \n {isStartingValidation ? $t.common?.loading : $t.dashboard?.run_llm_check}\n \n \n {$t.common?.refresh}\n \n
\n
\n\n" }, { "contract_id": "DashboardProfileOverrideIntegrationTest:Module", @@ -70534,7 +71613,7 @@ "contract_type": "Page", "file_path": "frontend/src/routes/dashboards/health/+page.svelte", "start_line": 1, - "end_line": 243, + "end_line": 249, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -70566,14 +71645,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

{$t.health?.title}

\n

{$t.health?.subtitle}

\n
\n\n
\n \n \n \n \n
\n
\n \n \n\n
\n \n
\n\n
\n
\n

{$t.health?.validation_details}

\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n {#if loading}\n {#each Array(3) as _, i (i)}\n \n \n \n \n \n \n \n \n {/each}\n {:else if healthData.items.length === 0}\n \n \n \n {:else}\n {#each healthData.items as item (item.record_id || item.dashboard_id)}\n \n \n \n \n \n \n \n \n {/each}\n {/if}\n \n
{$t.health?.table_dashboard}{$t.health?.table_environment}{$t.health?.table_status}{$t.health?.table_last_check}{$t.health?.table_summary}\n {$t.common?.actions}\n
\n {$t.health?.no_records}\n
\n \n \n {item.dashboard_slug || item.dashboard_id}\n \n \n {item.environment_id}\n \n \n {item.status}\n \n \n {formatDistanceToNow(new Date(item.last_check), { addSuffix: true })}\n \n {item.summary || '-'}\n \n {#if item.task_id}\n \n {$t.health?.view_report}\n {/if}\n handleDeleteReport(item)}\n disabled={deletingReportIds.has(item.record_id)}\n >\n {deletingReportIds.has(item.record_id) ? '...' : $t.common?.delete}\n \n
\n
\n
\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n
\n
\n
\n

{$t.health?.title}

\n

{$t.health?.subtitle}

\n
\n\n
\n \n \n \n \n
\n
\n \n \n\n
\n \n
\n\n
\n
\n

{$t.health?.validation_details}

\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n {#if loading}\n {#each Array(3) as _, i (i)}\n \n \n \n \n \n \n \n \n {/each}\n {:else if healthData.items.length === 0}\n \n \n \n {:else}\n {#each healthData.items as item (item.record_id || item.dashboard_id)}\n \n \n \n \n \n \n \n \n {/each}\n {/if}\n \n
{$t.health?.table_dashboard}{$t.health?.table_environment}{$t.health?.table_status}{$t.health?.table_last_check}{$t.health?.table_summary}\n {$t.common?.actions}\n
\n {$t.health?.no_records}\n
\n \n \n {item.dashboard_slug || item.dashboard_id}\n \n \n {item.environment_id}\n \n \n {item.status}\n \n \n {formatDistanceToNow(new Date(item.last_check), { addSuffix: true })}\n \n {item.summary || '-'}\n \n {#if item.task_id}\n \n {$t.health?.view_report}\n {/if}\n handleDeleteReport(item)}\n disabled={deletingReportIds.has(item.record_id)}\n >\n {deletingReportIds.has(item.record_id) ? '...' : $t.common?.delete}\n \n
\n
\n
\n
\n\n" }, { "contract_id": "loadData:Function", "contract_type": "Function", "file_path": "frontend/src/routes/dashboards/health/+page.svelte", "start_line": 35, - "end_line": 64, + "end_line": 70, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70586,14 +71665,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region loadData:Function [TYPE Function]\n // @PURPOSE: Load health summary rows and environment options for the current filter.\n // @PRE: Page is mounted or environment selection changed.\n // @POST: `healthData` and `environments` reflect latest backend response.\n // @SIDE_EFFECT: Calls backend API endpoints for health summary and environments.\n async function loadData() {\n loading = true;\n error = null;\n try {\n const summaryPromise = selectedEnvId\n ? getHealthSummary(selectedEnvId)\n : healthStore.refresh();\n const [summary, envs, consolidated] = await Promise.all([\n summaryPromise,\n getEnvironments(),\n getConsolidatedSettings().catch(() => ({ app_timezone: 'Europe/Moscow' }))\n ]);\n healthData = summary;\n environments = envs;\n appTimezone = consolidated.app_timezone || 'Europe/Moscow';\n if (!selectedEnvId && envs.length > 0) {\n // selectedEnvId = envs[0].id; // Optional: default to first env\n }\n } catch (e) {\n error = e.message;\n } finally {\n loading = false;\n }\n }\n // #endregion loadData:Function\n" + "body": " // #region loadData:Function [TYPE Function]\n // @PURPOSE: Load health summary rows and environment options for the current filter.\n // @PRE: Page is mounted or environment selection changed.\n // @POST: `healthData` and `environments` reflect latest backend response.\n // @SIDE_EFFECT: Calls backend API endpoints for health summary and environments.\n async function loadData() {\n loading = true;\n error = null;\n try {\n const summaryPromise = selectedEnvId\n ? getHealthSummary(selectedEnvId)\n : healthStore.refresh();\n const [summary, envs, consolidated] = await Promise.all([\n summaryPromise,\n getEnvironments(),\n getConsolidatedSettings().catch(() => ({ app_timezone: 'Europe/Moscow' }))\n ]);\n healthData = summary;\n environments = envs;\n appTimezone = consolidated.app_timezone || 'Europe/Moscow';\n if (!selectedEnvId && envs.length > 0) {\n // Pre-fill with active environment from global context\n const { get } = await import('svelte/store');\n const envCtx = await import('$lib/stores/environmentContext.js');\n const state = get(envCtx.environmentContextStore);\n if (state.selectedEnvId && envs.some((e) => e.id === state.selectedEnvId)) {\n selectedEnvId = state.selectedEnvId;\n }\n }\n } catch (e) {\n error = e.message;\n } finally {\n loading = false;\n }\n }\n // #endregion loadData:Function\n" }, { "contract_id": "handleEnvChange:Function", "contract_type": "Function", "file_path": "frontend/src/routes/dashboards/health/+page.svelte", - "start_line": 70, - "end_line": 78, + "start_line": 76, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70611,8 +71690,8 @@ "contract_id": "handleDeleteReport:Function", "contract_type": "Function", "file_path": "frontend/src/routes/dashboards/health/+page.svelte", - "start_line": 80, - "end_line": 104, + "start_line": 86, + "end_line": 110, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -70663,7 +71742,7 @@ "contract_type": "Page", "file_path": "frontend/src/routes/datasets/+page.svelte", "start_line": 1, - "end_line": 467, + "end_line": 472, "tier": "TIER_3", "complexity": 5, "metadata": { @@ -70673,7 +71752,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n
\n \n
\n

{$t.nav?.datasets}

\n
\n \n {$t.datasets?.all}: {stats?.total ?? 0}\n \n \n
\n
\n\n \n {#if dsError}\n
\n {dsError}\n \n
\n {/if}\n\n \n \n\n \n
\n \n
\n \n
\n\n \n {#if isMobile && mobileSlideOpen}\n \n \n \n \n
\n {detailData?.table_name ?? $t.datasets?.no_selection}\n \n
\n detailData && loadDetail(detailData.id)} envId={selectedEnv} />\n
\n {:else if !isMobile}\n
\n selectedDatasetId && loadDetail(selectedDatasetId)} envId={selectedEnv} />\n
\n {/if}\n
\n\n \n {#if selectedIds.size >= 2}\n
\n
\n ✓ {selectedIds.size} {$t.datasets?.selected}\n
\n \n \n \n
\n
\n
\n {/if}\n\n \n {#if showMapColumnsModal}\n
{ if (e.target === e.currentTarget) showMapColumnsModal = false; }}>\n
e.stopPropagation()}>\n

{$t.datasets?.bulk_map_columns}

\n

{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}

\n\n \n {$t.datasets?.source_type ?? 'Тип источника'}\n \n \n { if (mapSourceType === 'sqllab' && mapDatabases.length === 0 && selectedEnv) { api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); } }}\n >\n \n \n \n {#if mapSourceType === \"sqllab\"}\n \n \n \n \n \n \n {#each mapDatabases as db (db.id)}\n \n {/each}\n \n \n \n \n \n \n
\n {$t.datasets?.sql_templates ?? 'Шаблоны'}:\n { mapSqlQuery = \"SELECT c.column_name, c.data_type, pgd.description\\nFROM information_schema.columns c\\nLEFT JOIN pg_catalog.pg_description pgd ON pgd.objsubid = c.ordinal_position\\n AND pgd.objoid = (SELECT attrelid FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = c.table_name) AND attname = c.column_name)\\nWHERE c.table_schema = 'public'\\n AND c.table_name = 'your_table_name'\\nORDER BY c.ordinal_position;\" }}\n >information_schema\n { mapSqlQuery = \"SELECT column_name, description AS verbose_name\\nFROM my_column_mappings\\nWHERE table_name = 'your_table_name'\\nORDER BY column_name;\" }}\n >{$t.datasets?.sql_template_custom ?? 'Кастомная таблица'}\n { mapSqlQuery = \"\"; }}\n >{$t.datasets?.sql_template_clear ?? 'Очистить'}\n
\n \n {:else}\n \n \n \n \n mapFileData = e.target.files}\n />\n {/if}\n
\n \n \n
\n
\n
\n {/if}\n\n \n {#if showGenerateDocsModal}\n
{ if (e.target === e.currentTarget) showGenerateDocsModal = false; }}>\n
e.stopPropagation()}>\n

{$t.datasets?.bulk_docs_generation}

\n

{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}

\n \n {$t.datasets?.select_llm_provider ?? 'LLM-провайдер'}\n \n \n \n
\n \n \n
\n
\n
\n {/if}\n
\n\n" + "body": "\n\n\n
\n \n
\n

{$t.nav?.datasets}

\n
\n \n {$t.datasets?.all}: {stats?.total ?? 0}\n \n \n
\n
\n\n \n {#if dsError}\n
\n {dsError}\n \n
\n {/if}\n\n \n \n\n \n
\n \n
\n \n
\n\n \n {#if isMobile && mobileSlideOpen}\n \n \n \n \n
\n {detailData?.table_name ?? $t.datasets?.no_selection}\n \n
\n detailData && loadDetail(detailData.id)} envId={selectedEnv} />\n
\n {:else if !isMobile}\n
\n selectedDatasetId && loadDetail(selectedDatasetId)} envId={selectedEnv} />\n
\n {/if}\n
\n\n \n {#if selectedIds.size >= 2}\n
\n
\n ✓ {selectedIds.size} {$t.datasets?.selected}\n
\n \n \n \n
\n
\n
\n {/if}\n\n \n {#if showMapColumnsModal}\n
{ if (e.target === e.currentTarget) showMapColumnsModal = false; }}>\n
e.stopPropagation()}>\n

{$t.datasets?.bulk_map_columns}

\n

{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}

\n\n \n {$t.datasets?.source_type ?? 'Тип источника'}\n \n \n { if (mapSourceType === 'sqllab' && mapDatabases.length === 0 && selectedEnv) { api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); } }}\n >\n \n \n \n {#if mapSourceType === \"sqllab\"}\n \n \n \n \n \n \n {#each mapDatabases as db (db.id)}\n \n {/each}\n \n \n \n \n \n \n
\n {$t.datasets?.sql_templates ?? 'Шаблоны'}:\n { mapSqlQuery = \"SELECT c.column_name, c.data_type, pgd.description\\nFROM information_schema.columns c\\nLEFT JOIN pg_catalog.pg_description pgd ON pgd.objsubid = c.ordinal_position\\n AND pgd.objoid = (SELECT attrelid FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = c.table_name) AND attname = c.column_name)\\nWHERE c.table_schema = 'public'\\n AND c.table_name = 'your_table_name'\\nORDER BY c.ordinal_position;\" }}\n >information_schema\n { mapSqlQuery = \"SELECT column_name, description AS verbose_name\\nFROM my_column_mappings\\nWHERE table_name = 'your_table_name'\\nORDER BY column_name;\" }}\n >{$t.datasets?.sql_template_custom ?? 'Кастомная таблица'}\n { mapSqlQuery = \"\"; }}\n >{$t.datasets?.sql_template_clear ?? 'Очистить'}\n
\n \n {:else}\n \n \n \n \n mapFileData = e.target.files}\n />\n {/if}\n
\n \n \n
\n
\n
\n {/if}\n\n \n {#if showGenerateDocsModal}\n
{ if (e.target === e.currentTarget) showGenerateDocsModal = false; }}>\n
e.stopPropagation()}>\n

{$t.datasets?.bulk_docs_generation}

\n

{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}

\n \n {$t.datasets?.select_llm_provider ?? 'LLM-провайдер'}\n \n \n \n
\n \n \n
\n
\n
\n {/if}\n\n\n" }, { "contract_id": "ColumnsTable", @@ -70924,7 +72003,7 @@ "contract_type": "Page", "file_path": "frontend/src/routes/datasets/review/[id]/+page.svelte", "start_line": 1, - "end_line": 314, + "end_line": 343, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -70978,14 +72057,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n getWorkspaceStateLabel($t, s)} {profile} />\n\n {#if isBootstrapping}\n
{$t.dataset_review?.workspace?.loading}
\n {:else}\n {#if loadError}\n
{loadError}
\n {/if}\n\n {#if !session}\n \n {#if currentWorkspaceState === \"importing\"}\n
\n
\n
\n

{$t.dataset_review?.workspace?.import_progress_eyebrow}

\n

{$t.dataset_review?.workspace?.import_progress_title}

\n

{$t.dataset_review?.workspace?.import_progress_body}

\n
\n {getWorkspaceStateLabel($t, currentWorkspaceState)}\n
\n
    \n {#each importMilestones as m}\n
  1. \n
    \n \n {m.label}\n
    \n
  2. \n {/each}\n
\n
\n {/if}\n {#if submitError}\n
{submitError}
\n {/if}\n {:else}\n \n
\n
\n
\n
{$t.dataset_review?.workspace?.phase_card_label || \"Phase\"}
\n
{session.current_phase}
\n
\n
\n
{$t.dataset_review?.workspace?.preview_gate_card_label || \"Preview gate\"}
\n
{previewGateState}
\n
\n
\n
{$t.dataset_review?.workspace?.clarification_queue_card_label || \"Clarification queue\"}
\n
{clarificationRemainingCount} remaining / {clarificationTotalCount}
\n
\n
\n
{$t.dataset_review?.workspace?.launch_blockers_card_label || \"Launch blockers\"}
\n
{workspaceLaunchBlockers.length}
\n
\n
\n
\n\n \n
\n \n\n
\n
\n
\n
\n

AI orchestration

\n

{profile?.dataset_name || session.dataset_ref}

\n

{profile?.business_summary || assistantSeedPrompt}

\n
\n
\n
{$t.dataset_review?.workspace?.next_action_label}
\n
{getRecommendedActionLabel($t, session.recommended_action)}
\n
\n
\n
\n \n \n
\n {#if session.readiness_state === \"recovery_required\" || session.readiness_state === \"partially_ready\"}\n
{$t.dataset_review?.workspace?.partial_recovery_notice || \"Partial recovery mode.\"}
\n {/if}\n {#if importedFilters.length > 0}\n
\n
\n
{$t.dataset_review?.workspace?.recovered_filters_title || \"Recovered filters\"}
\n {importedFilters.length}\n
\n
\n {#each importedFilters.slice(0, 4) as filter}\n \n {/each}\n {#if importedFilters.length > 4}\n +{importedFilters.length - 4}\n {/if}\n
\n
\n {/if}\n
\n\n
\n\n \n\n
\n\n
\n
\n\n \n
\n {/if}\n {/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n getWorkspaceStateLabel($t, s)} {profile} />\n\n {#if isBootstrapping}\n
{$t.dataset_review?.workspace?.loading}
\n {:else}\n {#if loadError}\n
{loadError}
\n {/if}\n\n {#if !session}\n \n {#if currentWorkspaceState === \"importing\"}\n
\n
\n
\n

{$t.dataset_review?.workspace?.import_progress_eyebrow}

\n

{$t.dataset_review?.workspace?.import_progress_title}

\n

{$t.dataset_review?.workspace?.import_progress_body}

\n
\n {getWorkspaceStateLabel($t, currentWorkspaceState)}\n
\n
    \n {#each importMilestones as m}\n
  1. \n
    \n \n {m.label}\n
    \n
  2. \n {/each}\n
\n
\n {/if}\n {#if submitError}\n
{submitError}
\n {/if}\n {:else}\n \n
\n
\n
\n
{$t.dataset_review?.workspace?.phase_card_label || \"Phase\"}
\n
{session.current_phase}
\n
\n
\n
{$t.dataset_review?.workspace?.preview_gate_card_label || \"Preview gate\"}
\n
{previewGateState}
\n
\n
\n
{$t.dataset_review?.workspace?.clarification_queue_card_label || \"Clarification queue\"}
\n
{clarificationRemainingCount} remaining / {clarificationTotalCount}
\n
\n
\n
{$t.dataset_review?.workspace?.launch_blockers_card_label || \"Launch blockers\"}
\n
{workspaceLaunchBlockers.length}
\n
\n
\n
\n\n \n
\n \n\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n

{profile?.dataset_name || session.dataset_ref}

\n

{session.environment_id} · {session.dataset_ref}

\n
\n
\n {#if profile?.business_summary && profile.business_summary !== `Review session initialized for ${session.dataset_ref}.`}\n

{profile.business_summary}

\n {/if}\n
\n
\n
\n
{$t.dataset_review?.workspace?.next_action_label || \"Next step\"}
\n
{getRecommendedActionLabel($t, session.recommended_action)}
\n
\n {#if session.readiness_state === \"run_ready\"}\n \n \n Готов к запуску\n \n {:else if session.readiness_state === \"review_ready\"}\n \n \n Готов к проверке\n \n {:else if session.readiness_state === \"recovery_required\" || session.readiness_state === \"partially_ready\"}\n \n \n Частичное восстановление\n \n {/if}\n
\n
\n {#if importedFilters.length > 0}\n
\n
\n \n {$t.dataset_review?.workspace?.recovered_filters_title || \"Фильтры из дашборда\"}\n {importedFilters.length}\n
\n
\n {#each importedFilters.slice(0, 6) as filter}\n \n {/each}\n {#if importedFilters.length > 6}\n +{importedFilters.length - 6}\n {/if}\n
\n
\n {/if}\n
\n\n \n
\n\n \n\n
\n\n
\n
\n\n \n
\n {/if}\n {/if}\n
\n\n" }, { "contract_id": "DatasetReviewEntryUxTests", "contract_type": "Module", "file_path": "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.test.js", "start_line": 5, - "end_line": 288, + "end_line": 289, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -71013,14 +72092,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region DatasetReviewEntryUxTests [C:2] [TYPE Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, workspace-entry, resume, source-intake, ux-tests\n// @PURPOSE: Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [DatasetReviewWorkspaceEntry]\n// @UX_STATE: Loading -> workspace loader is shown before bootstrap completes.\n// @UX_STATE: ResumeList -> existing sessions render with summary fields and resume links.\n// @UX_STATE: ResumeEmpty -> empty-state copy is shown when no sessions are returned.\n// @UX_STATE: ResumeError -> inline error and retry action remain visible without removing new session flow.\n// @TEST_SCENARIO: renders_resume_sessions_and_new_session_intake\n// @TEST_SCENARIO: renders_empty_resume_state_when_no_sessions_exist\n// @TEST_SCENARIO: renders_resume_error_and_retry\n// @TEST_SCENARIO: submits_new_session_without_regression\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport DatasetReviewEntryPage from \"../+page.svelte\";\nimport { api } from \"$lib/api.js\";\n\nconst mockedGoto = vi.mocked(await import(\"$app/navigation\")).goto;\n\nfunction createSessionSummary(overrides = {}) {\n return {\n session_id: \"session-1\",\n user_id: \"user-1\",\n environment_id: \"env-1\",\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n dataset_ref: \"public.sales\",\n dataset_id: 101,\n readiness_state: \"partially_ready\",\n recommended_action: \"resume_session\",\n status: \"paused\",\n current_phase: \"review\",\n created_at: \"2026-03-18T06:00:00.000Z\",\n updated_at: \"2026-03-18T06:10:00.000Z\",\n last_activity_at: \"2026-03-18T06:15:00.000Z\",\n ...overrides,\n };\n}\n\nvi.mock(\"$app/navigation\", () => ({\n goto: vi.fn(),\n}));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n refresh: \"Refresh\",\n retry: \"Retry\",\n choose_environment: \"Choose environment\",\n not_available: \"N/A\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n workspace_entry: {\n resume_eyebrow: \"Resume session\",\n resume_title: \"Continue an existing review session\",\n resume_description:\n \"If a dataset review was already started, open the relevant session and continue from its latest saved state.\",\n resume_available_badge: \"Sessions available\",\n resume_loading: \"Loading existing sessions...\",\n resume_load_failed: \"Failed to load existing review sessions.\",\n resume_empty_title: \"No saved sessions yet\",\n resume_empty_body:\n \"Start a new review session from the source intake panel to make it available here for reopening later.\",\n resume_action: \"Resume\",\n session_id_label: \"Session ID\",\n dataset_ref_label: \"Dataset reference\",\n environment_id_label: \"Environment\",\n status_label: \"Status\",\n readiness_label: \"Readiness\",\n phase_label: \"Phase\",\n updated_at_label: \"Updated\",\n last_activity_at_label: \"Last activity\",\n },\n workspace: {\n eyebrow: \"Dataset review\",\n title: \"Dataset review workspace\",\n description: \"Review imported dataset context.\",\n state_label: \"Workspace state\",\n loading: \"Loading workspace\",\n state: {\n empty: \"Empty\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n fetchApi: vi.fn(),\n postApi: vi.fn(),\n requestApi: vi.fn(),\n },\n}));\n\nvi.mock(\"$lib/stores/environmentContext.js\", () => ({\n environmentContextStore: {\n subscribe: (run) => {\n run({\n environments: [{ id: \"env-1\", name: \"DEV\" }],\n selectedEnvId: \"env-1\",\n });\n return () => {};\n },\n },\n initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),\n}));\n\ndescribe(\"DatasetReviewWorkspaceEntry UX Contract\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockReset();\n api.postApi.mockReset();\n mockedGoto.mockReset();\n });\n\n it(\"renders_resume_sessions_and_new_session_intake\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [\n createSessionSummary(),\n createSessionSummary({\n session_id: \"session-2\",\n dataset_ref: \"analytics.daily_margin\",\n environment_id: \"env-2\",\n status: \"active\",\n readiness_state: \"review_ready\",\n current_phase: \"semantic_review\",\n }),\n ],\n total: 2,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions?page=1&page_size=5\",\n );\n });\n\n // Page header\n expect(screen.getByText(\"Dataset review\")).toBeDefined();\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Review imported dataset context.\")).toBeDefined();\n\n // Resume section header\n expect(screen.getByText(\"Continue an existing review session\")).toBeDefined();\n\n // Source intake section\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_empty_resume_state_when_no_sessions_exist\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [],\n total: 0,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalled();\n });\n\n // Page header renders regardless\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_resume_error_and_retry\", async () => {\n api.fetchApi.mockRejectedValueOnce(new Error(\"Session list failed\"));\n api.fetchApi.mockResolvedValueOnce({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n // Wait for initial fetch to complete (error is swallowed, but retry is possible via refresh button)\n await waitFor(() => {\n // After bootstrap, source intake should render\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n // Click the refresh button to retry session loading\n const refreshButton = screen.getByRole(\"button\", { name: \"Refresh\" });\n await fireEvent.click(refreshButton);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledTimes(2);\n });\n\n // Source intake still visible after retry\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"submits_new_session_without_regression\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n api.postApi.mockResolvedValue({ session_id: \"created-session\" });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n const environmentSelect = screen.getByRole(\"combobox\");\n const sourceInput = screen.getByPlaceholderText(\"https://superset.local/dashboard/10\");\n const submitButton = screen.getByRole(\"button\", { name: \"Start from link\" });\n\n await fireEvent.change(environmentSelect, { target: { value: \"env-1\" } });\n await fireEvent.input(sourceInput, {\n target: { value: \"https://superset.local/dashboard/10\" },\n });\n await fireEvent.click(submitButton);\n\n await waitFor(() => {\n expect(api.postApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions\", {\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n environment_id: \"env-1\",\n });\n });\n\n expect(mockedGoto).toHaveBeenCalledWith(\"/datasets/review/created-session\");\n });\n});\n\n// #endregion DatasetReviewEntryUxTests\n" + "body": "// #region DatasetReviewEntryUxTests [C:2] [TYPE Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, workspace-entry, resume, source-intake, ux-tests\n// @PURPOSE: Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [DatasetReviewWorkspaceEntry]\n// @UX_STATE: Loading -> workspace loader is shown before bootstrap completes.\n// @UX_STATE: ResumeList -> existing sessions render with summary fields and resume links.\n// @UX_STATE: ResumeEmpty -> empty-state copy is shown when no sessions are returned.\n// @UX_STATE: ResumeError -> inline error and retry action remain visible without removing new session flow.\n// @TEST_SCENARIO: renders_resume_sessions_and_new_session_intake\n// @TEST_SCENARIO: renders_empty_resume_state_when_no_sessions_exist\n// @TEST_SCENARIO: renders_resume_error_and_retry\n// @TEST_SCENARIO: submits_new_session_without_regression\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport DatasetReviewEntryPage from \"../+page.svelte\";\nimport { api } from \"$lib/api.js\";\n\nconst mockedGoto = vi.mocked(await import(\"$app/navigation\")).goto;\n\nfunction createSessionSummary(overrides = {}) {\n return {\n session_id: \"session-1\",\n user_id: \"user-1\",\n environment_id: \"env-1\",\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n dataset_ref: \"public.sales\",\n dataset_id: 101,\n readiness_state: \"partially_ready\",\n recommended_action: \"resume_session\",\n status: \"paused\",\n current_phase: \"review\",\n created_at: \"2026-03-18T06:00:00.000Z\",\n updated_at: \"2026-03-18T06:10:00.000Z\",\n last_activity_at: \"2026-03-18T06:15:00.000Z\",\n ...overrides,\n };\n}\n\nvi.mock(\"$app/navigation\", () => ({\n goto: vi.fn(),\n}));\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n refresh: \"Refresh\",\n retry: \"Retry\",\n choose_environment: \"Choose environment\",\n not_available: \"N/A\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n async_recovery_hint: \"Recovery steps run async\",\n },\n workspace_entry: {\n resume_eyebrow: \"Resume session\",\n resume_title: \"Continue an existing review session\",\n resume_description:\n \"If a dataset review was already started, open the relevant session and continue from its latest saved state.\",\n resume_available_badge: \"Sessions available\",\n resume_loading: \"Loading existing sessions...\",\n resume_load_failed: \"Failed to load existing review sessions.\",\n resume_empty_title: \"No saved sessions yet\",\n resume_empty_body:\n \"Start a new review session from the source intake panel to make it available here for reopening later.\",\n resume_action: \"Resume\",\n session_id_label: \"Session ID\",\n dataset_ref_label: \"Dataset reference\",\n environment_id_label: \"Environment\",\n status_label: \"Status\",\n readiness_label: \"Readiness\",\n phase_label: \"Phase\",\n updated_at_label: \"Updated\",\n last_activity_at_label: \"Last activity\",\n },\n workspace: {\n eyebrow: \"Dataset review\",\n title: \"Dataset review workspace\",\n description: \"Review imported dataset context.\",\n state_label: \"Workspace state\",\n loading: \"Loading workspace\",\n state: {\n empty: \"Empty\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n fetchApi: vi.fn(),\n postApi: vi.fn(),\n requestApi: vi.fn(),\n },\n}));\n\nvi.mock(\"$lib/stores/environmentContext.js\", () => ({\n environmentContextStore: {\n subscribe: (run) => {\n run({\n environments: [{ id: \"env-1\", name: \"DEV\" }],\n selectedEnvId: \"env-1\",\n });\n return () => {};\n },\n },\n initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),\n}));\n\ndescribe(\"DatasetReviewWorkspaceEntry UX Contract\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockReset();\n api.postApi.mockReset();\n mockedGoto.mockReset();\n });\n\n it(\"renders_resume_sessions_and_new_session_intake\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [\n createSessionSummary(),\n createSessionSummary({\n session_id: \"session-2\",\n dataset_ref: \"analytics.daily_margin\",\n environment_id: \"env-2\",\n status: \"active\",\n readiness_state: \"review_ready\",\n current_phase: \"semantic_review\",\n }),\n ],\n total: 2,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions?page=1&page_size=5\",\n );\n });\n\n // Page header\n expect(screen.getByText(\"Dataset review\")).toBeDefined();\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Review imported dataset context.\")).toBeDefined();\n\n // Resume section header\n expect(screen.getByText(\"Continue an existing review session\")).toBeDefined();\n\n // Source intake section\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_empty_resume_state_when_no_sessions_exist\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [],\n total: 0,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalled();\n });\n\n // Page header renders regardless\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_resume_error_and_retry\", async () => {\n api.fetchApi.mockRejectedValueOnce(new Error(\"Session list failed\"));\n api.fetchApi.mockResolvedValueOnce({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n // Wait for initial fetch to complete (error is swallowed, but retry is possible via refresh button)\n await waitFor(() => {\n // After bootstrap, source intake should render\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n // Click the refresh button to retry session loading\n const refreshButton = screen.getByRole(\"button\", { name: \"Refresh\" });\n await fireEvent.click(refreshButton);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledTimes(2);\n });\n\n // Source intake still visible after retry\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"submits_new_session_without_regression\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n api.postApi.mockResolvedValue({ session_id: \"created-session\" });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n const environmentSelect = screen.getByRole(\"combobox\");\n const sourceInput = screen.getByPlaceholderText(\"https://superset.local/dashboard/10\");\n const submitButton = screen.getByRole(\"button\", { name: \"Start from link\" });\n\n await fireEvent.change(environmentSelect, { target: { value: \"env-1\" } });\n await fireEvent.input(sourceInput, {\n target: { value: \"https://superset.local/dashboard/10\" },\n });\n await fireEvent.click(submitButton);\n\n await waitFor(() => {\n expect(api.postApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions\", {\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n environment_id: \"env-1\",\n });\n });\n\n expect(mockedGoto).toHaveBeenCalledWith(\"/datasets/review/created-session\");\n });\n});\n\n// #endregion DatasetReviewEntryUxTests\n" }, { "contract_id": "DatasetReviewEntryUxPageTests", "contract_type": "Module", "file_path": "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.ux.test.js", "start_line": 5, - "end_line": 287, + "end_line": 288, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -71048,14 +72127,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region DatasetReviewEntryUxPageTests [C:2] [TYPE Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, workspace-entry, resume, source-intake, ux-tests\n// @PURPOSE: Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [DatasetReviewWorkspaceEntry]\n// @UX_STATE: Loading -> workspace loader is shown before bootstrap completes.\n// @UX_STATE: ResumeList -> existing sessions render with summary fields and resume links.\n// @UX_STATE: ResumeEmpty -> empty-state copy is shown when no sessions are returned.\n// @UX_STATE: ResumeError -> inline error and retry action remain visible without removing new session flow.\n// @TEST_SCENARIO: renders_resume_sessions_and_new_session_intake\n// @TEST_SCENARIO: renders_empty_resume_state_when_no_sessions_exist\n// @TEST_SCENARIO: renders_resume_error_and_retry\n// @TEST_SCENARIO: submits_new_session_without_regression\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport DatasetReviewEntryPage from \"../+page.svelte\";\nimport { api } from \"$lib/api.js\";\n\nconst mockedGoto = vi.mocked(await import(\"$app/navigation\")).goto;\n\nvi.mock(\"$app/navigation\", () => ({\n goto: vi.fn(),\n}));\n\nfunction createSessionSummary(overrides = {}) {\n return {\n session_id: \"session-1\",\n user_id: \"user-1\",\n environment_id: \"env-1\",\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n dataset_ref: \"public.sales\",\n dataset_id: 101,\n readiness_state: \"partially_ready\",\n recommended_action: \"resume_session\",\n status: \"paused\",\n current_phase: \"review\",\n created_at: \"2026-03-18T06:00:00.000Z\",\n updated_at: \"2026-03-18T06:10:00.000Z\",\n last_activity_at: \"2026-03-18T06:15:00.000Z\",\n ...overrides,\n };\n}\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n refresh: \"Refresh\",\n retry: \"Retry\",\n choose_environment: \"Choose environment\",\n not_available: \"N/A\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n },\n workspace_entry: {\n resume_eyebrow: \"Resume session\",\n resume_title: \"Continue an existing review session\",\n resume_description:\n \"If a dataset review was already started, open the relevant session and continue from its latest saved state.\",\n resume_available_badge: \"Sessions available\",\n resume_loading: \"Loading existing sessions...\",\n resume_load_failed: \"Failed to load existing review sessions.\",\n resume_empty_title: \"No saved sessions yet\",\n resume_empty_body:\n \"Start a new review session from the source intake panel to make it available here for reopening later.\",\n resume_action: \"Resume\",\n session_id_label: \"Session ID\",\n dataset_ref_label: \"Dataset reference\",\n environment_id_label: \"Environment\",\n status_label: \"Status\",\n readiness_label: \"Readiness\",\n phase_label: \"Phase\",\n updated_at_label: \"Updated\",\n last_activity_at_label: \"Last activity\",\n },\n workspace: {\n eyebrow: \"Dataset review\",\n title: \"Dataset review workspace\",\n description: \"Review imported dataset context.\",\n state_label: \"Workspace state\",\n loading: \"Loading workspace\",\n state: {\n empty: \"Empty\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n fetchApi: vi.fn(),\n postApi: vi.fn(),\n requestApi: vi.fn(),\n },\n}));\n\nvi.mock(\"$lib/stores/environmentContext.js\", () => ({\n environmentContextStore: {\n subscribe: (run) => {\n run({\n environments: [{ id: \"env-1\", name: \"DEV\" }],\n selectedEnvId: \"env-1\",\n });\n return () => {};\n },\n },\n initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),\n}));\n\ndescribe(\"DatasetReviewWorkspaceEntry UX Contract\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockReset();\n api.postApi.mockReset();\n mockedGoto.mockReset();\n });\n\n it(\"renders_resume_sessions_and_new_session_intake\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [\n createSessionSummary(),\n createSessionSummary({\n session_id: \"session-2\",\n dataset_ref: \"analytics.daily_margin\",\n environment_id: \"env-2\",\n status: \"active\",\n readiness_state: \"review_ready\",\n current_phase: \"semantic_review\",\n }),\n ],\n total: 2,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions?page=1&page_size=5\",\n );\n });\n\n // Page header\n expect(screen.getByText(\"Dataset review\")).toBeDefined();\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Review imported dataset context.\")).toBeDefined();\n\n // Resume section header\n expect(screen.getByText(\"Continue an existing review session\")).toBeDefined();\n\n // Source intake section\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_empty_resume_state_when_no_sessions_exist\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [],\n total: 0,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalled();\n });\n\n // Page header renders regardless\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_resume_error_and_retry\", async () => {\n api.fetchApi.mockRejectedValueOnce(new Error(\"Session list failed\"));\n api.fetchApi.mockResolvedValueOnce({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n // Wait for initial fetch to complete (error is swallowed, retry via refresh button)\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n // Click the refresh button to retry session loading\n const refreshButton = screen.getByRole(\"button\", { name: \"Refresh\" });\n await fireEvent.click(refreshButton);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledTimes(2);\n });\n\n // Source intake still visible after retry\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"submits_new_session_without_regression\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n api.postApi.mockResolvedValue({ session_id: \"created-session\" });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n const environmentSelect = screen.getByRole(\"combobox\");\n const sourceInput = screen.getByPlaceholderText(\"https://superset.local/dashboard/10\");\n const submitButton = screen.getByRole(\"button\", { name: \"Start from link\" });\n\n await fireEvent.change(environmentSelect, { target: { value: \"env-1\" } });\n await fireEvent.input(sourceInput, {\n target: { value: \"https://superset.local/dashboard/10\" },\n });\n await fireEvent.click(submitButton);\n\n await waitFor(() => {\n expect(api.postApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions\", {\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n environment_id: \"env-1\",\n });\n });\n\n expect(mockedGoto).toHaveBeenCalledWith(\"/datasets/review/created-session\");\n });\n});\n\n// #endregion DatasetReviewEntryUxPageTests\n" + "body": "// #region DatasetReviewEntryUxPageTests [C:2] [TYPE Module]\n// @COMPLEXITY: 3\n// @SEMANTICS: dataset-review, workspace-entry, resume, source-intake, ux-tests\n// @PURPOSE: Verify dataset review entry route exposes resumable sessions alongside the new session intake flow.\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [DatasetReviewWorkspaceEntry]\n// @UX_STATE: Loading -> workspace loader is shown before bootstrap completes.\n// @UX_STATE: ResumeList -> existing sessions render with summary fields and resume links.\n// @UX_STATE: ResumeEmpty -> empty-state copy is shown when no sessions are returned.\n// @UX_STATE: ResumeError -> inline error and retry action remain visible without removing new session flow.\n// @TEST_SCENARIO: renders_resume_sessions_and_new_session_intake\n// @TEST_SCENARIO: renders_empty_resume_state_when_no_sessions_exist\n// @TEST_SCENARIO: renders_resume_error_and_retry\n// @TEST_SCENARIO: submits_new_session_without_regression\n\nimport { beforeEach, describe, expect, it, vi } from \"vitest\";\nimport { fireEvent, render, screen, waitFor } from \"@testing-library/svelte\";\nimport DatasetReviewEntryPage from \"../+page.svelte\";\nimport { api } from \"$lib/api.js\";\n\nconst mockedGoto = vi.mocked(await import(\"$app/navigation\")).goto;\n\nvi.mock(\"$app/navigation\", () => ({\n goto: vi.fn(),\n}));\n\nfunction createSessionSummary(overrides = {}) {\n return {\n session_id: \"session-1\",\n user_id: \"user-1\",\n environment_id: \"env-1\",\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n dataset_ref: \"public.sales\",\n dataset_id: 101,\n readiness_state: \"partially_ready\",\n recommended_action: \"resume_session\",\n status: \"paused\",\n current_phase: \"review\",\n created_at: \"2026-03-18T06:00:00.000Z\",\n updated_at: \"2026-03-18T06:10:00.000Z\",\n last_activity_at: \"2026-03-18T06:15:00.000Z\",\n ...overrides,\n };\n}\n\nvi.mock(\"$lib/i18n\", () => ({\n t: {\n subscribe: (fn) => {\n fn({\n common: {\n error: \"Common error\",\n refresh: \"Refresh\",\n retry: \"Retry\",\n choose_environment: \"Choose environment\",\n not_available: \"N/A\",\n },\n dataset_review: {\n source: {\n eyebrow: \"Source intake\",\n title: \"Start dataset review\",\n description: \"Paste link or provide dataset reference.\",\n state_idle: \"Idle\",\n state_validating: \"Validating\",\n state_rejected: \"Rejected\",\n environment_label: \"Environment\",\n environment_required: \"Environment is required\",\n superset_link_tab: \"Superset link\",\n superset_link_tab_hint: \"Paste dashboard or explore URL\",\n dataset_selection_tab: \"Dataset selection\",\n dataset_selection_tab_hint: \"Enter dataset ref\",\n superset_link_label: \"Superset link\",\n dataset_selection_label: \"Dataset reference\",\n superset_link_placeholder: \"https://superset.local/dashboard/10\",\n dataset_selection_placeholder: \"public.sales\",\n superset_link_hint: \"Paste a full Superset URL\",\n dataset_selection_hint: \"Provide schema.dataset reference\",\n recognized_link_hint: \"Recognized Superset link\",\n superset_link_required: \"Superset link is required\",\n dataset_selection_required: \"Dataset reference is required\",\n superset_link_invalid: \"Superset link must start with http\",\n submit_failed: \"Submit failed\",\n superset_link_recovery_note: \"You can fix the link inline\",\n dataset_selection_recovery_note: \"You can fix the dataset inline\",\n submitting: \"Submitting\",\n submit_superset_link: \"Start from link\",\n submit_dataset_selection: \"Start from dataset\",\n dataset_selection_acknowledged: \"Dataset selection acknowledged\",\n async_recovery_hint: \"Recovery steps run async\",\n },\n workspace_entry: {\n resume_eyebrow: \"Resume session\",\n resume_title: \"Continue an existing review session\",\n resume_description:\n \"If a dataset review was already started, open the relevant session and continue from its latest saved state.\",\n resume_available_badge: \"Sessions available\",\n resume_loading: \"Loading existing sessions...\",\n resume_load_failed: \"Failed to load existing review sessions.\",\n resume_empty_title: \"No saved sessions yet\",\n resume_empty_body:\n \"Start a new review session from the source intake panel to make it available here for reopening later.\",\n resume_action: \"Resume\",\n session_id_label: \"Session ID\",\n dataset_ref_label: \"Dataset reference\",\n environment_id_label: \"Environment\",\n status_label: \"Status\",\n readiness_label: \"Readiness\",\n phase_label: \"Phase\",\n updated_at_label: \"Updated\",\n last_activity_at_label: \"Last activity\",\n },\n workspace: {\n eyebrow: \"Dataset review\",\n title: \"Dataset review workspace\",\n description: \"Review imported dataset context.\",\n state_label: \"Workspace state\",\n loading: \"Loading workspace\",\n state: {\n empty: \"Empty\",\n },\n },\n },\n });\n return () => {};\n },\n },\n}));\n\nvi.mock(\"$lib/api.js\", () => ({\n api: {\n fetchApi: vi.fn(),\n postApi: vi.fn(),\n requestApi: vi.fn(),\n },\n}));\n\nvi.mock(\"$lib/stores/environmentContext.js\", () => ({\n environmentContextStore: {\n subscribe: (run) => {\n run({\n environments: [{ id: \"env-1\", name: \"DEV\" }],\n selectedEnvId: \"env-1\",\n });\n return () => {};\n },\n },\n initializeEnvironmentContext: vi.fn().mockResolvedValue(undefined),\n}));\n\ndescribe(\"DatasetReviewWorkspaceEntry UX Contract\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n api.fetchApi.mockReset();\n api.postApi.mockReset();\n mockedGoto.mockReset();\n });\n\n it(\"renders_resume_sessions_and_new_session_intake\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [\n createSessionSummary(),\n createSessionSummary({\n session_id: \"session-2\",\n dataset_ref: \"analytics.daily_margin\",\n environment_id: \"env-2\",\n status: \"active\",\n readiness_state: \"review_ready\",\n current_phase: \"semantic_review\",\n }),\n ],\n total: 2,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledWith(\n \"/dataset-orchestration/sessions?page=1&page_size=5\",\n );\n });\n\n // Page header\n expect(screen.getByText(\"Dataset review\")).toBeDefined();\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Review imported dataset context.\")).toBeDefined();\n\n // Resume section header\n expect(screen.getByText(\"Continue an existing review session\")).toBeDefined();\n\n // Source intake section\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_empty_resume_state_when_no_sessions_exist\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [],\n total: 0,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalled();\n });\n\n // Page header renders regardless\n expect(screen.getByText(\"Dataset review workspace\")).toBeDefined();\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"renders_resume_error_and_retry\", async () => {\n api.fetchApi.mockRejectedValueOnce(new Error(\"Session list failed\"));\n api.fetchApi.mockResolvedValueOnce({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n\n render(DatasetReviewEntryPage);\n\n // Wait for initial fetch to complete (error is swallowed, retry via refresh button)\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n // Click the refresh button to retry session loading\n const refreshButton = screen.getByRole(\"button\", { name: \"Refresh\" });\n await fireEvent.click(refreshButton);\n\n await waitFor(() => {\n expect(api.fetchApi).toHaveBeenCalledTimes(2);\n });\n\n // Source intake still visible after retry\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n it(\"submits_new_session_without_regression\", async () => {\n api.fetchApi.mockResolvedValue({\n items: [createSessionSummary()],\n total: 1,\n page: 1,\n page_size: 20,\n has_next: false,\n });\n api.postApi.mockResolvedValue({ session_id: \"created-session\" });\n\n render(DatasetReviewEntryPage);\n\n await waitFor(() => {\n expect(screen.getByText(\"Start dataset review\")).toBeDefined();\n });\n\n const environmentSelect = screen.getByRole(\"combobox\");\n const sourceInput = screen.getByPlaceholderText(\"https://superset.local/dashboard/10\");\n const submitButton = screen.getByRole(\"button\", { name: \"Start from link\" });\n\n await fireEvent.change(environmentSelect, { target: { value: \"env-1\" } });\n await fireEvent.input(sourceInput, {\n target: { value: \"https://superset.local/dashboard/10\" },\n });\n await fireEvent.click(submitButton);\n\n await waitFor(() => {\n expect(api.postApi).toHaveBeenCalledWith(\"/dataset-orchestration/sessions\", {\n source_kind: \"superset_link\",\n source_input: \"https://superset.local/dashboard/10\",\n environment_id: \"env-1\",\n });\n });\n\n expect(mockedGoto).toHaveBeenCalledWith(\"/datasets/review/created-session\");\n });\n});\n\n// #endregion DatasetReviewEntryUxPageTests\n" }, { "contract_id": "ReviewWorkspaceHelpers", "contract_type": "Module", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", "start_line": 1, - "end_line": 120, + "end_line": 145, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -71067,7 +72146,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region ReviewWorkspaceHelpers [C:2] [TYPE Module] [SEMANTICS dataset-review, helpers, derived, utils]\n// @BRIEF Shared helper functions for the dataset review workspace.\n// @RATIONALE Extracted from DatasetReviewWorkspace to reduce page script size per INV_7.\n\n/**\n * Build import milestones for the workspace progress tracking.\n */\nexport function buildImportMilestones(t, intakeAcknowledgment, session, importedFilters, latestPreview, profile, findings, currentWorkspaceState) {\n const recognizedDone = Boolean(intakeAcknowledgment || session?.dataset_ref);\n const filtersDone = importedFilters.length > 0;\n const variablesDone = Boolean(importedFilters.length || latestPreview);\n const semanticsDone = Boolean(profile?.business_summary || findings.length > 0);\n const importState = currentWorkspaceState === \"importing\";\n\n return [\n { key: \"recognized\", label: t.dataset_review?.workspace?.import_milestones?.recognized, state: recognizedDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"filters\", label: t.dataset_review?.workspace?.import_milestones?.filters, state: filtersDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"variables\", label: t.dataset_review?.workspace?.import_milestones?.variables, state: variablesDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"semantic_sources\", label: t.dataset_review?.workspace?.import_milestones?.semantic_sources, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"summary\", label: t.dataset_review?.workspace?.import_milestones?.summary, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n ];\n}\n\n/**\n * Build workspace launch blockers list.\n */\nexport function buildWorkspaceLaunchBlockers(t, session, unresolvedBlockingFindingsCount, pendingApprovalMappingsCount, previewGateState) {\n if (!session) return [];\n const blockers = [];\n if (unresolvedBlockingFindingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.blocking_findings || \"Blocking findings\"}: ${unresolvedBlockingFindingsCount}`);\n if (pendingApprovalMappingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.mapping_approvals || \"Mapping approvals\"}: ${pendingApprovalMappingsCount}`);\n if (previewGateState !== \"ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.preview_state || \"SQL preview state\"}: ${previewGateState}`);\n if (String(session.readiness_state || \"\") !== \"run_ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.readiness || \"Readiness\"}: ${session.readiness_state || \"unknown\"}`);\n return blockers;\n}\n\n/**\n * Build assistant seed prompt from session context.\n */\nexport function buildAssistantSeedPrompt(t, session, profile, workspaceLaunchBlockers, clarificationCurrentQuestion, importedFilters) {\n if (!session) return \"\";\n const intro = [\n `Session ${session.session_id}`,\n `Dataset ${profile?.dataset_name || session.dataset_ref}`,\n `Phase ${session.current_phase || \"review\"}`,\n `Readiness ${session.readiness_state || \"unknown\"}`,\n ];\n if (workspaceLaunchBlockers.length > 0)\n intro.push(`Launch blockers: ${workspaceLaunchBlockers.join(\"; \")}`);\n if (clarificationCurrentQuestion?.question_text)\n intro.push(`Active clarification: ${clarificationCurrentQuestion.question_text}`);\n else if (importedFilters.length > 0)\n intro.push(`Recovered filters: ${importedFilters.length}`);\n return intro.join(\". \");\n}\n\n/**\n * Build assistant context prompt for contextual queries.\n */\nexport function buildAssistantContextPrompt(t, payload) {\n const label = payload?.label || payload?.target || \"current item\";\n const section = payload?.section || \"workspace\";\n const context = String(payload?.prompt || \"\").trim();\n if (payload?.mode === \"improve\") {\n return `${t.dataset_review?.workspace?.assistant_context?.improve_prefix || \"Improve\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n }\n return `${t.dataset_review?.workspace?.assistant_context?.ask_prefix || \"Help review\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n}\n\n/**\n * Get workspace state label from i18n.\n */\nexport function getWorkspaceStateLabel(t, state) {\n return t.dataset_review?.workspace?.state?.[state] || state;\n}\n\n/**\n * Get recommended action label from i18n.\n */\nexport function getRecommendedActionLabel(t, action) {\n const normalized = String(action || \"import_from_superset\");\n return t.dataset_review?.workspace?.actions?.[normalized] || normalized;\n}\n\n/**\n * Get primary action CTA label.\n */\nexport function getPrimaryActionCtaLabel(t, session) {\n if (!session) return t.dataset_review?.workspace?.next_action_label;\n if (session.recommended_action === \"resume_session\") return t.dataset_review?.workspace?.resume_action;\n if (session.recommended_action === \"generate_sql_preview\") return t.dataset_review?.preview?.generate_action;\n if (session.recommended_action === \"launch_dataset\") return t.dataset_review?.launch?.launch_action;\n return t.dataset_review?.workspace?.next_action_label;\n}\n\n/**\n * Merge a single item into a collection by id key, replacing if exists.\n */\nexport function mergeCollectionItem(collection = [], item = null, idKey = \"id\") {\n if (!item) return collection;\n const itemId = item?.[idKey];\n if (!itemId) return collection;\n return collection.some((entry) => entry?.[idKey] === itemId)\n ? collection.map((entry) => (entry?.[idKey] === itemId ? { ...entry, ...item } : entry))\n : [...collection, item];\n}\n\n/**\n * Stringify a value for display.\n */\nexport function stringifyValue(t, value) {\n if (value === null || value === undefined || value === \"\") return t.common?.not_available || \"N/A\";\n if (typeof value === \"string\") return value;\n try { return JSON.stringify(value); } catch (_) { return String(value); }\n}\n// #endregion ReviewWorkspaceHelpers\n" + "body": "// #region ReviewWorkspaceHelpers [C:2] [TYPE Module] [SEMANTICS dataset-review, helpers, derived, utils]\n// @BRIEF Shared helper functions for the dataset review workspace.\n// @RATIONALE Extracted from DatasetReviewWorkspace to reduce page script size per INV_7.\n\n/**\n * Build import milestones for the workspace progress tracking.\n */\nexport function buildImportMilestones(t, intakeAcknowledgment, session, importedFilters, latestPreview, profile, findings, currentWorkspaceState) {\n const recognizedDone = Boolean(intakeAcknowledgment || session?.dataset_ref);\n const filtersDone = importedFilters.length > 0;\n const variablesDone = Boolean(importedFilters.length || latestPreview);\n const semanticsDone = Boolean(profile?.business_summary || findings.length > 0);\n const importState = currentWorkspaceState === \"importing\";\n\n return [\n { key: \"recognized\", label: t.dataset_review?.workspace?.import_milestones?.recognized, state: recognizedDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"filters\", label: t.dataset_review?.workspace?.import_milestones?.filters, state: filtersDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"variables\", label: t.dataset_review?.workspace?.import_milestones?.variables, state: variablesDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"semantic_sources\", label: t.dataset_review?.workspace?.import_milestones?.semantic_sources, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n { key: \"summary\", label: t.dataset_review?.workspace?.import_milestones?.summary, state: semanticsDone ? \"done\" : importState ? \"active\" : \"pending\" },\n ];\n}\n\n/**\n * Build workspace launch blockers list.\n */\nexport function buildWorkspaceLaunchBlockers(t, session, unresolvedBlockingFindingsCount, pendingApprovalMappingsCount, previewGateState) {\n if (!session) return [];\n const blockers = [];\n if (unresolvedBlockingFindingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.blocking_findings || \"Blocking findings\"}: ${unresolvedBlockingFindingsCount}`);\n if (pendingApprovalMappingsCount > 0)\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.mapping_approvals || \"Mapping approvals\"}: ${pendingApprovalMappingsCount}`);\n if (previewGateState !== \"ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.preview_state || \"SQL preview state\"}: ${previewGateState}`);\n if (String(session.readiness_state || \"\") !== \"run_ready\")\n blockers.push(`${t.dataset_review?.workspace?.launch_blockers?.readiness || \"Readiness\"}: ${session.readiness_state || \"unknown\"}`);\n return blockers;\n}\n\n/**\n * Build assistant seed prompt from session context.\n * @RATIONALE Human-readable prompt in user's language helps the assistant provide relevant context.\n * Technical details (session_id, phase) are omitted — the assistant already has them via API context.\n */\nexport function buildAssistantSeedPrompt(t, session, profile, workspaceLaunchBlockers, clarificationCurrentQuestion, importedFilters) {\n if (!session) return \"\";\n const datasetName = profile?.dataset_name || session.dataset_ref || \"датасет\";\n const summary = profile?.business_summary || \"\";\n \n const parts = [];\n \n // Opening: what dataset we're reviewing\n parts.push(`Я начинаю review датасета \"${datasetName}\".`);\n \n // Business summary if available\n if (summary && summary !== `Review session initialized for ${session.dataset_ref}.`) {\n parts.push(`Описание: ${summary}`);\n }\n \n // Filters status\n if (importedFilters.length > 0) {\n const filterNames = importedFilters.slice(0, 3).map(f => f.display_name || f.filter_name).join(\", \");\n const extra = importedFilters.length > 3 ? ` и ещё ${importedFilters.length - 3}` : \"\";\n parts.push(`Восстановлены фильтры из дашборда: ${filterNames}${extra}.`);\n }\n \n // Active clarification takes priority\n if (clarificationCurrentQuestion?.question_text) {\n parts.push(`Есть вопрос для уточнения: ${clarificationCurrentQuestion.question_text}`);\n }\n \n // Readiness hint\n const readiness = session.readiness_state || \"\";\n if (readiness === \"review_ready\") {\n parts.push(\"Датасет готов к проверке. Помоги мне понять, что нужно сделать дальше.\");\n } else if (readiness === \"recovery_required\") {\n parts.push(\"Контекст восстановлен частично. Помоги разобраться, чего не хватает.\");\n } else if (readiness === \"run_ready\") {\n parts.push(\"Датасет готов к запуску. Проверь, всё ли в порядке.\");\n }\n \n return parts.join(\" \");\n}\n\n/**\n * Build assistant context prompt for contextual queries.\n */\nexport function buildAssistantContextPrompt(t, payload) {\n const label = payload?.label || payload?.target || \"current item\";\n const section = payload?.section || \"workspace\";\n const context = String(payload?.prompt || \"\").trim();\n if (payload?.mode === \"improve\") {\n return `${t.dataset_review?.workspace?.assistant_context?.improve_prefix || \"Improve\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n }\n return `${t.dataset_review?.workspace?.assistant_context?.ask_prefix || \"Help review\"} ${label}. ${t.dataset_review?.workspace?.assistant_context?.section_label || \"Section\"}: ${section}.${context ? ` ${t.dataset_review?.workspace?.assistant_context?.context_label || \"Context\"}: ${context}` : \"\"}`;\n}\n\n/**\n * Get workspace state label from i18n.\n */\nexport function getWorkspaceStateLabel(t, state) {\n return t.dataset_review?.workspace?.state?.[state] || state;\n}\n\n/**\n * Get recommended action label from i18n.\n */\nexport function getRecommendedActionLabel(t, action) {\n const normalized = String(action || \"import_from_superset\");\n return t.dataset_review?.workspace?.actions?.[normalized] || normalized;\n}\n\n/**\n * Get primary action CTA label.\n */\nexport function getPrimaryActionCtaLabel(t, session) {\n if (!session) return t.dataset_review?.workspace?.next_action_label;\n if (session.recommended_action === \"resume_session\") return t.dataset_review?.workspace?.resume_action;\n if (session.recommended_action === \"generate_sql_preview\") return t.dataset_review?.preview?.generate_action;\n if (session.recommended_action === \"launch_dataset\") return t.dataset_review?.launch?.launch_action;\n return t.dataset_review?.workspace?.next_action_label;\n}\n\n/**\n * Merge a single item into a collection by id key, replacing if exists.\n */\nexport function mergeCollectionItem(collection = [], item = null, idKey = \"id\") {\n if (!item) return collection;\n const itemId = item?.[idKey];\n if (!itemId) return collection;\n return collection.some((entry) => entry?.[idKey] === itemId)\n ? collection.map((entry) => (entry?.[idKey] === itemId ? { ...entry, ...item } : entry))\n : [...collection, item];\n}\n\n/**\n * Stringify a value for display.\n */\nexport function stringifyValue(t, value) {\n if (value === null || value === undefined || value === \"\") return t.common?.not_available || \"N/A\";\n if (typeof value === \"string\") return value;\n try { return JSON.stringify(value); } catch (_) { return String(value); }\n}\n// #endregion ReviewWorkspaceHelpers\n" }, { "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildImportMilestones", @@ -71104,22 +72183,24 @@ "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", "start_line": 41, - "end_line": 44, + "end_line": 46, "tier": "TIER_1", "complexity": 1, - "metadata": {}, + "metadata": { + "RATIONALE": "Human-readable prompt in user's language helps the assistant provide relevant context. Technical details (session_id, phase) are omitted — the assistant already has them via API context." + }, "relations": [], "schema_warnings": [], "anchor_syntax": "jsdoc", "tier_source": "AutoCalculated", - "body": "/**\n * Build assistant seed prompt from session context.\n */" + "body": "/**\n * Build assistant seed prompt from session context.\n * @RATIONALE Human-readable prompt in user's language helps the assistant provide relevant context.\n * Technical details (session_id, phase) are omitted — the assistant already has them via API context.\n */" }, { "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::buildAssistantContextPrompt", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 61, - "end_line": 64, + "start_line": 86, + "end_line": 89, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71133,8 +72214,8 @@ "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getWorkspaceStateLabel", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 74, - "end_line": 77, + "start_line": 99, + "end_line": 102, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71148,8 +72229,8 @@ "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getRecommendedActionLabel", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 81, - "end_line": 84, + "start_line": 106, + "end_line": 109, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71163,8 +72244,8 @@ "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::getPrimaryActionCtaLabel", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 89, - "end_line": 92, + "start_line": 114, + "end_line": 117, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71178,8 +72259,8 @@ "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::mergeCollectionItem", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 100, - "end_line": 103, + "start_line": 125, + "end_line": 128, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71193,8 +72274,8 @@ "contract_id": "frontend/src/routes/datasets/review/review-workspace-helpers.js::stringifyValue", "contract_type": "Function", "file_path": "frontend/src/routes/datasets/review/review-workspace-helpers.js", - "start_line": 112, - "end_line": 115, + "start_line": 137, + "end_line": 140, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71359,171 +72440,434 @@ "tier_source": "AutoCalculated", "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n
\n

\n {$t.maintenance?.title || \"Maintenance Banners\"}\n

\n maintenanceStore.loadEvents()}\n disabled={pageLoading}\n class=\"inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50\"\n >\n {$t.common?.refresh || \"Refresh\"}\n \n
\n\n {#if pageLoading}\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n {#each [1, 2, 3] as _, i (i)}\n
\n {/each}\n
\n
\n {:else}\n \n maintenanceStore.loadEvents()} />\n\n \n \n\n \n \n {/if}\n
\n\n" }, + { + "contract_id": "MigrationPage", + "contract_type": "Page", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 1, + "end_line": 955, + "tier": "TIER_2", + "complexity": 4, + "metadata": { + "BRIEF": "Main migration dashboard page for environment selection, dry-run validation, and migration execution.", + "COMPLEXITY": 4, + "INVARIANT": "Migration start is blocked unless source and target environments are selected, distinct, and at least one dashboard is selected.", + "LAYER": "UI", + "SEMANTICS": [ + "migration", + "dashboard", + "environment", + "dry-run", + "task-resume" + ], + "TEST_CONTRACT": "[DashboardSelection + Environment IDs] -> [DryRunResult | TaskStartResult | ValidationError]", + "TEST_EDGE": "external_fail -> API failures set error state and preserve recoverable UI controls.", + "TEST_FIXTURE": "migration_dry_run_fixture -> file:backend/tests/fixtures/migration_dry_run_fixture.json", + "TEST_INVARIANT": "migration_guardrails_enforced -> VERIFIED_BY: [start_migration_valid_selection, start_dry_run_valid_selection, missing_field]", + "TEST_SCENARIO": "awaiting_input_task_selected -> Opens password prompt with requested databases.", + "UX_FEEDBACK": "Inline error banner, disabled CTA states, loading labels, dry-run summary cards, modal dialogs.", + "UX_REACTIVITY": "State transitions rely on Svelte reactive bindings and store subscription to selectedTask.", + "UX_RECOVERY": "User can adjust selection, refresh databases, retry dry-run/migration, resume task with passwords, or cancel modal flow.", + "UX_STATE": "[Success] -> Dry-run summary or active task view is rendered after successful API operations." + }, + "relations": [ + { + "source_id": "MigrationPage", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + }, + { + "source_id": "MigrationPage", + "relation_type": "DEPENDS_ON", + "target_id": "EXT:frontend:stores", + "target_ref": "[EXT:frontend:stores]" + }, + { + "source_id": "MigrationPage", + "relation_type": "DEPENDS_ON", + "target_id": "EXT:frontend:taskService", + "target_ref": "[EXT:frontend:taskService]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "EnvSelector", + "target_ref": "[EnvSelector]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "DashboardGrid", + "target_ref": "[DashboardGrid]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "MappingTable", + "target_ref": "[MappingTable]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "EXT:frontend:TaskRunner", + "target_ref": "[EXT:frontend:TaskRunner]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "TaskHistory", + "target_ref": "[TaskHistory]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "TaskLogViewer", + "target_ref": "[TaskLogViewer]" + }, + { + "source_id": "MigrationPage", + "relation_type": "BINDS_TO", + "target_id": "PasswordPrompt", + "target_ref": "[PasswordPrompt]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n\n\n\n\n\n
\n \n
\n \n showTaskHistory = !showTaskHistory}\n class=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-900 transition-colors\"\n >\n \n \n \n {$t.tasks?.recent || \"Task History\"}\n \n \n \n \n
\n \n\n {#if $selectedTask}\n \n
\n
\n

\n {$t.tasks?.task_label || \"Task\"}\n

\n \n
\n \n
\n \n {:else}\n \n {#if showTaskHistory}\n
\n \n
\n {/if}\n \n\n \n
\n
\n {#each [\n { step: 1, label: $t.migration?.source_env || \"Source\" },\n { step: 2, label: $t.migration?.select_dashboards_title || \"Dashboards\" },\n { step: 3, label: $t.tasks?.summary_report || \"Review\" },\n { step: 4, label: $t.migration?.start || \"Migrate\" },\n ] as s}\n goToStep(s.step)}\n disabled={s.step > currentStep && !((s.step === 2 && stepReady[1]) || (s.step === 3 && stepReady[1] && stepReady[2]) || (s.step === 4 && dryRunResult))}\n class=\"flex flex-col items-center group\"\n >\n
s.step\n ? 'bg-indigo-600 border-indigo-600 text-white'\n : currentStep === s.step\n ? 'bg-indigo-50 border-indigo-500 text-indigo-700'\n : 'bg-white border-gray-300 text-gray-400'\n }\n ${(s.step <= currentStep || (s.step === 2 && stepReady[1]) || (s.step === 3 && stepReady[1] && stepReady[2])) ? 'cursor-pointer hover:border-indigo-400' : 'cursor-not-allowed'}\n `}>\n {#if currentStep > s.step}\n \n \n \n {:else}\n {s.step}\n {/if}\n
\n \n \n {#if s.step < 4}\n
\n
\n
s.step ? '100%' : '0%'}\">
\n
\n
\n {/if}\n {/each}\n
\n
\n \n\n \n {#if loading}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n \n {:else if error}\n
\n
\n
\n \n \n \n
\n
\n

{error}

\n
\n \n
\n
\n {/if}\n\n \n {#if currentStep === 1}\n \n
\n
\n 1\n

{$t.migration?.source_env || \"Source Environment\"} → {$t.migration?.target_env || \"Target Environment\"}

\n
\n
\n
\n
\n
\n

{$t.migration?.source_env || \"Source\"}

\n
\n \n
\n
\n
\n
\n

{$t.migration?.target_env || \"Target\"}

\n
\n \n
\n
\n {#if sourceEnvId && targetEnvId && sourceEnvId === targetEnvId}\n
\n

\n \n \n \n {$t.migration?.different_envs || \"Source and target environments must be different.\"}\n

\n
\n {/if}\n
\n \n
\n
\n
\n {/if}\n \n\n \n {#if currentStep === 2}\n \n
\n
\n 2\n

{$t.migration?.select_dashboards_title || \"Select Dashboards\"}

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

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

\n {/if}\n\n \n
\n

{$t.dashboard?.settings || \"Options\"}

\n
\n \n\n \n
\n
\n\n \n {#if replaceDb}\n
\n
\n

{$t.migration?.database_mappings || \"Database Mappings\"}

\n \n \n \n \n {$t.migration?.fetch_dbs || \"Fetch Databases\"}\n \n
\n {#if fetchingDbs}\n
\n \n \n \n {$t.migration?.loading_dbs || \"Loading databases and suggestions...\"}\n
\n {:else if sourceDatabases.length > 0}\n \n {:else if sourceEnvId && targetEnvId}\n

{$t.migration?.mapping_hint || \"Select environments and click \\\"Fetch Databases\\\" to start mapping.\"}

\n {/if}\n
\n {/if}\n\n
\n \n
\n \n {#if dryRunLoading}\n \n {/if}\n {$t.dashboard?.view_summary || \"Dry Run\"}\n \n
\n
\n
\n
\n {/if}\n \n\n \n {#if currentStep === 3}\n \n
\n
\n 3\n

{$t.tasks?.summary_report || \"Review & Validate\"}

\n
\n\n {#if dryRunResult}\n \n
\n
\n \n \n \n

{$t.dashboard?.selected_dashboards || \"Selected Dashboards\"}

\n
\n
\n {#each dryRunResult.selected_dashboard_titles || [] as title}\n \n {title}\n \n {/each}\n
\n
\n\n \n
\n \n
\n
\n
\n \n \n \n
\n

{$t.dashboard?.title || \"Dashboards\"}

\n
\n
\n
\n Create\n {dryRunResult.summary.dashboards.create}\n
\n
\n Update\n {dryRunResult.summary.dashboards.update}\n
\n
\n Delete\n {dryRunResult.summary.dashboards.delete}\n
\n
\n
\n\n \n
\n
\n
\n \n \n \n
\n

{$t.dashboard?.charts || \"Charts\"}

\n
\n
\n
\n Create\n {dryRunResult.summary.charts.create}\n
\n
\n Update\n {dryRunResult.summary.charts.update}\n
\n
\n Delete\n {dryRunResult.summary.charts.delete}\n
\n
\n
\n\n \n
\n
\n
\n \n \n \n
\n

{$t.dashboard?.linked_resources || \"Datasets\"}

\n
\n
\n
\n Create\n {dryRunResult.summary.datasets.create}\n
\n
\n Update\n {dryRunResult.summary.datasets.update}\n
\n
\n Delete\n {dryRunResult.summary.datasets.delete}\n
\n
\n
\n
\n\n \n
\n
\n
\n \n \n \n

{$t.tasks?.issues || \"Risk Assessment\"}

\n
\n
\n \n
\n Score:\n
\n {dryRunResult.risk.score}/100\n
\n
\n \n
\n {dryRunResult.risk.level.toUpperCase()}\n
\n
\n
\n\n \n
\n 20 && dryRunResult.risk.score <= 50}\n class:bg-red-500={dryRunResult.risk.score > 50}\n style=\"width: {dryRunResult.risk.score}%\"\n >
\n
\n\n \n {#if dryRunResult.risk.items && dryRunResult.risk.items.length > 0}\n
\n {#each dryRunResult.risk.items as item}\n
\n
\n
\n

{item.message}

\n

{item.object_type}: {item.object_uuid}

\n
\n
\n {/each}\n
\n {:else}\n

No issues detected.

\n {/if}\n
\n\n \n
\n \n \n \n \n {$t.tasks?.diff_preview || \"Diff JSON\"}\n \n
{JSON.stringify(dryRunResult, null, 2)}
\n
\n {:else}\n
\n \n \n \n

Run a dry-run to see the migration summary here.

\n
\n {/if}\n\n
\n \n {\n currentStep = 4;\n startMigration();\n }}\n disabled={!dryRunResult}\n >\n {$t.migration?.start || \"Start Migration\"}\n \n
\n
\n \n {/if}\n \n\n \n {#if currentStep === 4 && !$selectedTask}\n \n
\n \n \n \n

{$t.migration?.starting || \"Starting Migration...\"}

\n

{$t.common.loading || \"Please wait...\"}

\n \n
\n
\n {/if}\n \n\n \n {#if !currentStep || currentStep === 0}\n \n
\n \n \n \n

{$t.dashboard?.migrate_modal_title || \"Select environments to start\"}

\n \n
\n
\n {/if}\n {/if}\n\n\n\n {\n console.info(\"[MigrationModals][REASON] Closing log viewer\");\n showLogViewer = false;\n }}\n/>\n\n {\n console.info(\"[MigrationModals][REASON] User cancelled password prompt\");\n showPasswordPrompt = false;\n }}\n/>\n\n\n\n" + }, { "contract_id": "ReactiveDashboardFetch:Block", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 163, - "end_line": 175, + "start_line": 144, + "end_line": 150, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Automatically fetch dashboards when the source environment is changed.", - "POST": "fetchDashboards is called with the new sourceEnvId.", - "PRE": "sourceEnvId is not empty.", - "UX_STATE": "[Loading] -> Triggered when sourceEnvId changes." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region ReactiveDashboardFetch:Block [TYPE Function]\n /**\n * @BRIEF: Automatically fetch dashboards when the source environment is changed.\n * @PRE: sourceEnvId is not empty.\n * @POST: fetchDashboards is called with the new sourceEnvId.\n * @UX_STATE: [Loading] -> Triggered when sourceEnvId changes.\n */\n $effect(() => {\n if (!sourceEnvId) return;\n console.info(\"[ReactiveDashboardFetch][REASON] Source environment changed, fetching dashboards\", { sourceEnvId });\n void fetchDashboards(sourceEnvId);\n });\n // #endregion ReactiveDashboardFetch:Block\n" + "body": " // #region ReactiveDashboardFetch:Block [TYPE Function]\n $effect(() => {\n if (!sourceEnvId) return;\n console.info(\"[ReactiveDashboardFetch][REASON] Source environment changed, fetching dashboards\", { sourceEnvId });\n void fetchDashboards(sourceEnvId);\n });\n // #endregion ReactiveDashboardFetch:Block\n" }, { "contract_id": "handleMappingUpdate:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 225, - "end_line": 264, + "start_line": 195, + "end_line": 229, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Saves a mapping to the backend.", - "POST": "Mapping is saved and local mappings list is updated.", - "PRE": "event.detail contains sourceUuid and targetUuid." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleMappingUpdate:Function [TYPE Function]\n /**\n * @purpose Saves a mapping to the backend.\n * @pre event.detail contains sourceUuid and targetUuid.\n * @post Mapping is saved and local mappings list is updated.\n */\n async function handleMappingUpdate(event: CustomEvent) {\n return belief_scope(\"handleMappingUpdate\", async () => {\n const { sourceUuid, targetUuid } = event.detail;\n const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);\n const tDb = targetDatabases.find((d) => d.uuid === targetUuid);\n\n if (!sDb || !tDb) {\n console.warn(\"[handleMappingUpdate][EXPLORE] Database not found for mapping\", { sourceUuid, targetUuid });\n return;\n }\n\n console.info(\"[handleMappingUpdate][REASON] Updating database mapping\", { sourceUuid, targetUuid });\n try {\n const savedMapping = await api.postApi(\"/mappings\", {\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n source_db_uuid: sourceUuid,\n target_db_uuid: targetUuid,\n source_db_name: sDb.database_name,\n target_db_name: tDb.database_name,\n });\n\n mappings = [\n ...mappings.filter((m) => m.source_db_uuid !== sourceUuid),\n savedMapping,\n ];\n console.info(\"[handleMappingUpdate][REFLECT] Mapping saved successfully\");\n } catch (e) {\n console.error(\"[handleMappingUpdate][EXPLORE] Failed to save mapping\", e);\n error = e.message;\n }\n });\n }\n // #endregion handleMappingUpdate:Function\n" + "body": " // #region handleMappingUpdate:Function [TYPE Function]\n async function handleMappingUpdate(event: CustomEvent) {\n return belief_scope(\"handleMappingUpdate\", async () => {\n const { sourceUuid, targetUuid } = event.detail;\n const sDb = sourceDatabases.find((d) => d.uuid === sourceUuid);\n const tDb = targetDatabases.find((d) => d.uuid === targetUuid);\n\n if (!sDb || !tDb) {\n console.warn(\"[handleMappingUpdate][EXPLORE] Database not found for mapping\", { sourceUuid, targetUuid });\n return;\n }\n\n console.info(\"[handleMappingUpdate][REASON] Updating database mapping\", { sourceUuid, targetUuid });\n try {\n const savedMapping = await api.postApi(\"/mappings\", {\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n source_db_uuid: sourceUuid,\n target_db_uuid: targetUuid,\n source_db_name: sDb.database_name,\n target_db_name: tDb.database_name,\n });\n\n mappings = [\n ...mappings.filter((m) => m.source_db_uuid !== sourceUuid),\n savedMapping,\n ];\n console.info(\"[handleMappingUpdate][REFLECT] Mapping saved successfully\");\n } catch (e) {\n console.error(\"[handleMappingUpdate][EXPLORE] Failed to save mapping\", e);\n error = e.message;\n }\n });\n }\n // #endregion handleMappingUpdate:Function\n" }, { "contract_id": "handleViewLogs:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 266, - "end_line": 278, + "start_line": 231, + "end_line": 240, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Opens the log viewer for a specific task.", - "POST": "logViewer state updated and showLogViewer set to true.", - "PRE": "event.detail contains task object." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleViewLogs:Function [TYPE Function]\n // @BRIEF: Opens the log viewer for a specific task.\n // @PRE: event.detail contains task object.\n // @POST: logViewer state updated and showLogViewer set to true.\n function handleViewLogs(event: CustomEvent) {\n return belief_scope(\"handleViewLogs\", () => {\n const task = event.detail;\n logViewerTaskId = task.id;\n logViewerTaskStatus = task.status;\n showLogViewer = true;\n });\n }\n // #endregion handleViewLogs:Function\n" + "body": " // #region handleViewLogs:Function [TYPE Function]\n function handleViewLogs(event: CustomEvent) {\n return belief_scope(\"handleViewLogs\", () => {\n const task = event.detail;\n logViewerTaskId = task.id;\n logViewerTaskStatus = task.status;\n showLogViewer = true;\n });\n }\n // #endregion handleViewLogs:Function\n" }, { "contract_id": "handlePasswordPrompt:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 280, - "end_line": 314, + "start_line": 242, + "end_line": 262, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Reactive logic to show password prompt when a task is awaiting input.", - "POST": "showPasswordPrompt set to true with request data.", - "PRE": "selectedTask status is AWAITING_INPUT." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handlePasswordPrompt:Function [TYPE Function]\n // @BRIEF: Reactive logic to show password prompt when a task is awaiting input.\n // @PRE: selectedTask status is AWAITING_INPUT.\n // @POST: showPasswordPrompt set to true with request data.\n // This is triggered by TaskRunner or TaskHistory when a task needs input\n // For now, we rely on the WebSocket or manual check.\n // Ideally, TaskHistory or TaskRunner emits an event when input is needed.\n // Or we watch selectedTask.\n\n // #region ReactivePasswordPrompt:Block [TYPE Function]\n /**\n * @BRIEF: Monitor selected task for input requests and trigger password prompt.\n * @PRE: $selectedTask is not null and status is AWAITING_INPUT.\n * @POST: showPasswordPrompt is set to true if input_request is database_password.\n * @UX_STATE: [AwaitingInput] -> Password prompt modal is displayed.\n */\n $effect(() => {\n const activeTask = selectedTaskState.current;\n if (\n !activeTask ||\n activeTask.status !== \"AWAITING_INPUT\" ||\n !activeTask.input_request\n ) {\n return;\n }\n const req = activeTask.input_request;\n if (req.type === \"database_password\") {\n console.info(\"[ReactivePasswordPrompt][REASON] Task awaiting database passwords\", { taskId: activeTask.id });\n passwordPromptDatabases = req.databases || [];\n passwordPromptErrorMessage = req.error_message || \"\";\n showPasswordPrompt = true;\n }\n });\n // #endregion ReactivePasswordPrompt:Block\n // #endregion handlePasswordPrompt:Function\n" + "body": " // #region handlePasswordPrompt:Function [TYPE Function]\n // #region ReactivePasswordPrompt:Block [TYPE Function]\n $effect(() => {\n const activeTask = selectedTaskState.current;\n if (\n !activeTask ||\n activeTask.status !== \"AWAITING_INPUT\" ||\n !activeTask.input_request\n ) {\n return;\n }\n const req = activeTask.input_request;\n if (req.type === \"database_password\") {\n console.info(\"[ReactivePasswordPrompt][REASON] Task awaiting database passwords\", { taskId: activeTask.id });\n passwordPromptDatabases = req.databases || [];\n passwordPromptErrorMessage = req.error_message || \"\";\n showPasswordPrompt = true;\n }\n });\n // #endregion ReactivePasswordPrompt:Block\n // #endregion handlePasswordPrompt:Function\n" }, { "contract_id": "ReactivePasswordPrompt:Block", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 289, - "end_line": 313, + "start_line": 243, + "end_line": 261, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Monitor selected task for input requests and trigger password prompt.", - "POST": "showPasswordPrompt is set to true if input_request is database_password.", - "PRE": "$selectedTask is not null and status is AWAITING_INPUT.", - "UX_STATE": "[AwaitingInput] -> Password prompt modal is displayed." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region ReactivePasswordPrompt:Block [TYPE Function]\n /**\n * @BRIEF: Monitor selected task for input requests and trigger password prompt.\n * @PRE: $selectedTask is not null and status is AWAITING_INPUT.\n * @POST: showPasswordPrompt is set to true if input_request is database_password.\n * @UX_STATE: [AwaitingInput] -> Password prompt modal is displayed.\n */\n $effect(() => {\n const activeTask = selectedTaskState.current;\n if (\n !activeTask ||\n activeTask.status !== \"AWAITING_INPUT\" ||\n !activeTask.input_request\n ) {\n return;\n }\n const req = activeTask.input_request;\n if (req.type === \"database_password\") {\n console.info(\"[ReactivePasswordPrompt][REASON] Task awaiting database passwords\", { taskId: activeTask.id });\n passwordPromptDatabases = req.databases || [];\n passwordPromptErrorMessage = req.error_message || \"\";\n showPasswordPrompt = true;\n }\n });\n // #endregion ReactivePasswordPrompt:Block\n" + "body": " // #region ReactivePasswordPrompt:Block [TYPE Function]\n $effect(() => {\n const activeTask = selectedTaskState.current;\n if (\n !activeTask ||\n activeTask.status !== \"AWAITING_INPUT\" ||\n !activeTask.input_request\n ) {\n return;\n }\n const req = activeTask.input_request;\n if (req.type === \"database_password\") {\n console.info(\"[ReactivePasswordPrompt][REASON] Task awaiting database passwords\", { taskId: activeTask.id });\n passwordPromptDatabases = req.databases || [];\n passwordPromptErrorMessage = req.error_message || \"\";\n showPasswordPrompt = true;\n }\n });\n // #endregion ReactivePasswordPrompt:Block\n" }, { "contract_id": "handleResumeMigration:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 316, - "end_line": 337, + "start_line": 264, + "end_line": 280, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Resumes a migration task with provided passwords.", - "POST": "resumeTask is called and showPasswordPrompt is hidden on success.", - "PRE": "event.detail contains passwords." - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region handleResumeMigration:Function [TYPE Function]\n // @BRIEF: Resumes a migration task with provided passwords.\n // @PRE: event.detail contains passwords.\n // @POST: resumeTask is called and showPasswordPrompt is hidden on success.\n async function handleResumeMigration(event: CustomEvent) {\n return belief_scope(\"handleResumeMigration\", async () => {\n if (!selectedTaskState.current) return;\n\n const { passwords } = event.detail;\n try {\n await resumeTask(selectedTaskState.current.id, passwords);\n showPasswordPrompt = false;\n // Task status update will be handled by store/websocket\n } catch (e) {\n console.error(\"Failed to resume task:\", e);\n passwordPromptErrorMessage =\n e.message || $t.migration?.resume_failed;\n // Keep prompt open\n }\n });\n }\n // #endregion handleResumeMigration:Function\n" + "body": " // #region handleResumeMigration:Function [TYPE Function]\n async function handleResumeMigration(event: CustomEvent) {\n return belief_scope(\"handleResumeMigration\", async () => {\n if (!selectedTaskState.current) return;\n\n const { passwords } = event.detail;\n try {\n await resumeTask(selectedTaskState.current.id, passwords);\n showPasswordPrompt = false;\n } catch (e) {\n console.error(\"Failed to resume task:\", e);\n passwordPromptErrorMessage =\n e.message || $t.migration?.resume_failed;\n }\n });\n }\n // #endregion handleResumeMigration:Function\n" }, { "contract_id": "startMigration:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 339, - "end_line": 414, + "start_line": 282, + "end_line": 340, "tier": "TIER_1", "complexity": 1, - "metadata": { - "BRIEF": "Initiates the migration process by sending the selection to the backend.", - "POST": "A migration task is created and selectedTask store is updated.", - "PRE": "sourceEnvId and targetEnvId are set and different; at least one dashboard is selected.", - "SIDE_EFFECT": "Resets dryRunResult; updates error state on failure.", - "UX_STATE": "[Loading] -> [Success] or [Error]" - }, + "metadata": {}, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region startMigration:Function [TYPE Function]\n /**\n * @BRIEF: Initiates the migration process by sending the selection to the backend.\n * @PRE: sourceEnvId and targetEnvId are set and different; at least one dashboard is selected.\n * @POST: A migration task is created and selectedTask store is updated.\n * @SIDE_EFFECT: Resets dryRunResult; updates error state on failure.\n * @UX_STATE: [Loading] -> [Success] or [Error]\n */\n async function startMigration() {\n return belief_scope(\"startMigration\", async () => {\n if (!sourceEnvId || !targetEnvId) {\n console.warn(\"[startMigration][EXPLORE] Missing environment selection\");\n error =\n $t.migration?.select_both_envs ||\n \"Please select both source and target environments.\";\n return;\n }\n if (sourceEnvId === targetEnvId) {\n console.warn(\"[startMigration][EXPLORE] Source and target environments are identical\");\n error =\n $t.migration?.different_envs ||\n \"Source and target environments must be different.\";\n return;\n }\n if (selectedDashboardIds.length === 0) {\n console.warn(\"[startMigration][EXPLORE] No dashboards selected\");\n error =\n $t.migration?.select_dashboards ||\n \"Please select at least one dashboard to migrate.\";\n return;\n }\n\n console.info(\"[startMigration][REASON] Initiating migration execution\", {\n sourceEnvId,\n targetEnvId,\n dashboardCount: selectedDashboardIds.length\n });\n error = \"\";\n try {\n dryRunResult = null;\n const selection: DashboardSelection = {\n selected_ids: selectedDashboardIds,\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n replace_db_config: replaceDb,\n fix_cross_filters: fixCrossFilters,\n };\n \n const result = await api.postApi(\"/migration/execute\", selection);\n console.info(\"[startMigration][REFLECT] Migration task created\", { taskId: result.task_id });\n\n // Wait a brief moment for the backend to ensure the task is retrievable\n await new Promise((r) => setTimeout(r, 500));\n\n // Fetch full task details and switch to TaskRunner view\n try {\n const task = await api.getTask(result.task_id);\n selectedTask.set(task);\n console.info(\"[startMigration][REFLECT] Task details fetched and store updated\");\n } catch (fetchErr) {\n console.warn(\"[startMigration][EXPLORE] Could not fetch task details immediately, using placeholder\", fetchErr);\n selectedTask.set({\n id: result.task_id,\n plugin_id: \"superset-migration\",\n status: \"RUNNING\",\n logs: [],\n params: {},\n });\n }\n } catch (e) {\n console.error(\"[startMigration][EXPLORE] Migration initiation failed\", e);\n error = e.message;\n }\n });\n }\n // #endregion startMigration:Function\n" + "body": " // #region startMigration:Function [TYPE Function]\n async function startMigration() {\n return belief_scope(\"startMigration\", async () => {\n if (!sourceEnvId || !targetEnvId) {\n console.warn(\"[startMigration][EXPLORE] Missing environment selection\");\n error = $t.migration?.select_both_envs || \"Please select both source and target environments.\";\n return;\n }\n if (sourceEnvId === targetEnvId) {\n console.warn(\"[startMigration][EXPLORE] Source and target environments are identical\");\n error = $t.migration?.different_envs || \"Source and target environments must be different.\";\n return;\n }\n if (selectedDashboardIds.length === 0) {\n console.warn(\"[startMigration][EXPLORE] No dashboards selected\");\n error = $t.migration?.select_dashboards || \"Please select at least one dashboard to migrate.\";\n return;\n }\n\n console.info(\"[startMigration][REASON] Initiating migration execution\", {\n sourceEnvId, targetEnvId, dashboardCount: selectedDashboardIds.length\n });\n error = \"\";\n try {\n dryRunResult = null;\n const selection: DashboardSelection = {\n selected_ids: selectedDashboardIds,\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n replace_db_config: replaceDb,\n fix_cross_filters: fixCrossFilters,\n };\n\n const result = await api.postApi(\"/migration/execute\", selection);\n console.info(\"[startMigration][REFLECT] Migration task created\", { taskId: result.task_id });\n\n await new Promise((r) => setTimeout(r, 500));\n\n try {\n const task = await api.getTask(result.task_id);\n selectedTask.set(task);\n console.info(\"[startMigration][REFLECT] Task details fetched and store updated\");\n } catch (fetchErr) {\n console.warn(\"[startMigration][EXPLORE] Could not fetch task details immediately, using placeholder\", fetchErr);\n selectedTask.set({\n id: result.task_id,\n plugin_id: \"superset-migration\",\n status: \"RUNNING\",\n logs: [],\n params: {},\n });\n }\n } catch (e) {\n console.error(\"[startMigration][EXPLORE] Migration initiation failed\", e);\n error = e.message;\n }\n });\n }\n // #endregion startMigration:Function\n" }, { "contract_id": "startDryRun:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/+page.svelte", - "start_line": 416, - "end_line": 471, + "start_line": 342, + "end_line": 384, "tier": "TIER_1", "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " // #region startDryRun:Function [TYPE Function]\n async function startDryRun() {\n return belief_scope(\"startDryRun\", async () => {\n if (!sourceEnvId || !targetEnvId) {\n console.warn(\"[startDryRun][EXPLORE] Missing environment selection\");\n error = $t.migration?.select_both_envs || \"Please select both source and target environments.\";\n return;\n }\n if (sourceEnvId === targetEnvId) {\n console.warn(\"[startDryRun][EXPLORE] Source and target environments are identical\");\n error = $t.migration?.different_envs || \"Source and target environments must be different.\";\n return;\n }\n if (selectedDashboardIds.length === 0) {\n console.warn(\"[startDryRun][EXPLORE] No dashboards selected\");\n error = $t.migration?.select_dashboards || \"Please select at least one dashboard to migrate.\";\n return;\n }\n\n console.info(\"[startDryRun][REASON] Initiating dry-run analysis\", { sourceEnvId, targetEnvId });\n error = \"\";\n dryRunLoading = true;\n try {\n const selection: DashboardSelection = {\n selected_ids: selectedDashboardIds,\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n replace_db_config: replaceDb,\n fix_cross_filters: fixCrossFilters,\n };\n dryRunResult = await api.postApi(\"/migration/dry-run\", selection);\n console.info(\"[startDryRun][REFLECT] Dry-run analysis completed\", { riskScore: dryRunResult.risk.score });\n currentStep = 3; // Move to review step on successful dry-run\n } catch (e) {\n console.error(\"[startDryRun][EXPLORE] Dry-run analysis failed\", e);\n error = e.message;\n dryRunResult = null;\n } finally {\n dryRunLoading = false;\n }\n });\n }\n // #endregion startDryRun:Function\n" + }, + { + "contract_id": "MigrationHeader", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 396, + "end_line": 412, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " \n
\n \n showTaskHistory = !showTaskHistory}\n class=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:text-gray-900 transition-colors\"\n >\n \n \n \n {$t.tasks?.recent || \"Task History\"}\n \n \n \n \n
\n \n" + }, + { + "contract_id": "ActiveTaskSection", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 415, + "end_line": 427, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " \n
\n
\n

\n {$t.tasks?.task_label || \"Task\"}\n

\n \n
\n \n
\n \n" + }, + { + "contract_id": "TaskHistoryPanel", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 429, + "end_line": 435, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " \n {#if showTaskHistory}\n
\n \n
\n {/if}\n \n" + }, + { + "contract_id": "StepProgressBar", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 437, + "end_line": 483, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " \n
\n
\n {#each [\n { step: 1, label: $t.migration?.source_env || \"Source\" },\n { step: 2, label: $t.migration?.select_dashboards_title || \"Dashboards\" },\n { step: 3, label: $t.tasks?.summary_report || \"Review\" },\n { step: 4, label: $t.migration?.start || \"Migrate\" },\n ] as s}\n goToStep(s.step)}\n disabled={s.step > currentStep && !((s.step === 2 && stepReady[1]) || (s.step === 3 && stepReady[1] && stepReady[2]) || (s.step === 4 && dryRunResult))}\n class=\"flex flex-col items-center group\"\n >\n
s.step\n ? 'bg-indigo-600 border-indigo-600 text-white'\n : currentStep === s.step\n ? 'bg-indigo-50 border-indigo-500 text-indigo-700'\n : 'bg-white border-gray-300 text-gray-400'\n }\n ${(s.step <= currentStep || (s.step === 2 && stepReady[1]) || (s.step === 3 && stepReady[1] && stepReady[2])) ? 'cursor-pointer hover:border-indigo-400' : 'cursor-not-allowed'}\n `}>\n {#if currentStep > s.step}\n \n \n \n {:else}\n {s.step}\n {/if}\n
\n \n \n {#if s.step < 4}\n
\n
\n
s.step ? '100%' : '0%'}\">
\n
\n
\n {/if}\n {/each}\n
\n
\n \n" + }, + { + "contract_id": "MigrationModals", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/+page.svelte", + "start_line": 932, + "end_line": 953, + "tier": "TIER_1", + "complexity": 1, + "metadata": {}, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "\n {\n console.info(\"[MigrationModals][REASON] Closing log viewer\");\n showLogViewer = false;\n }}\n/>\n\n {\n console.info(\"[MigrationModals][REASON] User cancelled password prompt\");\n showPasswordPrompt = false;\n }}\n/>\n\n" + }, + { + "contract_id": "TestMigrationDashboard", + "contract_type": "Module", + "file_path": "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js", + "start_line": 1, + "end_line": 17, + "tier": "TIER_2", + "complexity": 3, "metadata": { - "BRIEF": "Performs a dry-run migration to identify potential risks and changes.", - "POST": "dryRunResult is populated with the pre-flight analysis.", - "PRE": "source/target environments and selected dashboards are valid.", - "UX_FEEDBACK": "User sees summary cards + risk block + JSON details after success.", - "UX_RECOVERY": "User can adjust selection and press Dry Run again.", - "UX_STATE": "[Loading] -> [Success] or [Error]" + "BRIEF": "Verify migration page wizard UX — environment selection, step navigation, error recovery, dry-run flow.", + "COMPLEXITY": 3, + "LAYER": "UI", + "TEST_CONTRACT": "[DashboardSelection + Environment IDs] -> [StepTransition | DryRunResult | ErrorState]", + "TEST_EDGE": "external_fail -> API failure surfaces error banner that can be dismissed.", + "TEST_INVARIANT": "migration_guardrails_enforced -> VERIFIED_BY: [step1_next_disabled_without_env, api_error_shows_banner]", + "TEST_SCENARIO": "step_nav_back_button -> Back button returns to previous step." + }, + "relations": [ + { + "source_id": "TestMigrationDashboard", + "relation_type": "BINDS_TO", + "target_id": "MigrationPage", + "target_ref": "[MigrationPage]" + }, + { + "source_id": "TestMigrationDashboard", + "relation_type": "DEPENDS_ON", + "target_id": "ApiModule", + "target_ref": "[ApiModule]" + }, + { + "source_id": "TestMigrationDashboard", + "relation_type": "DEPENDS_ON", + "target_id": "I18nModule", + "target_ref": "[I18nModule]" + }, + { + "source_id": "TestMigrationDashboard", + "relation_type": "DEPENDS_ON", + "target_id": "EXT:@testing-library/svelte", + "target_ref": "[EXT:@testing-library/svelte]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region TestMigrationDashboard [C:3] [TYPE Module] [SEMANTICS test,migration,ux]\n// @BRIEF Verify migration page wizard UX — environment selection, step navigation, error recovery, dry-run flow.\n// @RELATION BINDS_TO -> [MigrationPage]\n// @LAYER UI\n// @RELATION DEPENDS_ON -> [ApiModule]\n// @RELATION DEPENDS_ON -> [I18nModule]\n// @RELATION DEPENDS_ON -> [EXT:@testing-library/svelte]\n// @TEST_CONTRACT: [DashboardSelection + Environment IDs] -> [StepTransition | DryRunResult | ErrorState]\n// @TEST_SCENARIO: environments_loaded_on_mount -> Page fetches environments on mount.\n// @TEST_SCENARIO: step1_next_disabled_without_env -> Next button disabled when no environments selected.\n// @TEST_SCENARIO: api_error_shows_banner -> API failure surfaces error with dismiss button.\n// @TEST_SCENARIO: step2_dashboards_shown -> Step 2 content renders when navigated with envs selected.\n// @TEST_SCENARIO: step_nav_back_button -> Back button returns to previous step.\n// @TEST_EDGE: missing_field -> Empty env selection blocks Next and shows appropriate state.\n// @TEST_EDGE: external_fail -> API failure surfaces error banner that can be dismissed.\n// @TEST_INVARIANT: migration_guardrails_enforced -> VERIFIED_BY: [step1_next_disabled_without_env, api_error_shows_banner]\n// #endregion TestMigrationDashboard\n" + }, + { + "contract_id": "describe:MigratonDashboard", + "contract_type": "Block", + "file_path": "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js", + "start_line": 95, + "end_line": 170, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Test suite for migration page UX contract — step wizard, error handling, navigation.", + "COMPLEXITY": 2 + }, + "relations": [ + { + "source_id": "describe:MigratonDashboard", + "relation_type": "BINDS_TO", + "target_id": "MigrationPage", + "target_ref": "[MigrationPage]" + } + ], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region describe:MigratonDashboard [C:2] [TYPE Block]\n// @BRIEF Test suite for migration page UX contract — step wizard, error handling, navigation.\n// @RELATION BINDS_TO -> [MigrationPage]\ndescribe('MigrationDashboard.ux.test.js', () => {\n const mockEnvironments = [\n { id: 'env-1', name: 'Source Env', url: 'https://source.example.com' },\n { id: 'env-2', name: 'Target Env', url: 'https://target.example.com' }\n ];\n\n beforeEach(() => {\n vi.clearAllMocks();\n api.getEnvironmentsList.mockResolvedValue(mockEnvironments);\n api.requestApi.mockResolvedValue([]);\n api.postApi.mockResolvedValue({});\n });\n\n // #region test_renders_and_fetches_envs [C:2] [TYPE Function]\n // @BRIEF Page fetches environments on mount.\n // @TEST_SCENARIO: environments_loaded_on_mount\n it('renders and fetches environments on mount', async () => {\n render(MigrationDashboard);\n expect(api.getEnvironmentsList).toHaveBeenCalled();\n });\n // #endregion test_renders_and_fetches_envs\n\n // #region test_step1_next_disabled [C:2] [TYPE Function]\n // @BRIEF Next button in step 1 is disabled without environment selection.\n // @TEST_SCENARIO: step1_next_disabled_without_env\n // @TEST_EDGE: missing_field\n it('has step 1 shown by default with Next button disabled', async () => {\n const { getByRole, queryByText } = render(MigrationDashboard);\n\n // Step 1 is shown by default — find the Next button\n const nextBtn = getByRole('button', { name: /next/i });\n expect(nextBtn).toBeTruthy();\n // Next button should be disabled until environments selected\n expect(nextBtn.hasAttribute('disabled')).toBe(true);\n\n // Task History toggle should be present\n const taskHistoryBtn = queryByText(/task|последние задачи/i);\n expect(taskHistoryBtn).toBeTruthy();\n });\n // #endregion test_step1_next_disabled\n\n // #region test_error_state [C:2] [TYPE Function]\n // @BRIEF API failure surfaces error banner that can be dismissed.\n // @TEST_SCENARIO: api_error_shows_banner\n // @TEST_EDGE: external_fail\n it('handles error state gracefully when environments fail to load', async () => {\n api.getEnvironmentsList.mockRejectedValueOnce(new Error('API Error'));\n const { getByText } = render(MigrationDashboard);\n await waitFor(() => {\n expect(getByText('API Error')).toBeTruthy();\n });\n\n // Error dismiss button should exist (close/aria-label)\n const dismissBtn = getByText('API Error').closest('[class*=\"bg-red\"]')?.querySelector('button');\n expect(dismissBtn).toBeTruthy();\n });\n // #endregion test_error_state\n\n // #region test_ui_has_header_and_toggle [C:2] [TYPE Function]\n // @BRIEF Page header and task history toggle render correctly.\n it('renders page header and task history toggle button', async () => {\n const { getByText } = render(MigrationDashboard);\n\n // Header title should be present\n expect(getByText('Migration')).toBeTruthy();\n // Task history toggle button should exist\n const toggleBtn = getByText(/task|последние задачи/i);\n expect(toggleBtn).toBeTruthy();\n expect(toggleBtn.tagName).toBe('BUTTON');\n });\n // #endregion test_ui_has_header_and_toggle\n});\n// #endregion describe:MigratonDashboard\n" + }, + { + "contract_id": "test_renders_and_fetches_envs", + "contract_type": "Function", + "file_path": "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js", + "start_line": 111, + "end_line": 118, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Page fetches environments on mount.", + "COMPLEXITY": 2, + "TEST_SCENARIO": "environments_loaded_on_mount" }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region startDryRun:Function [TYPE Function]\n /**\n * @BRIEF: Performs a dry-run migration to identify potential risks and changes.\n * @PRE: source/target environments and selected dashboards are valid.\n * @POST: dryRunResult is populated with the pre-flight analysis.\n * @UX_STATE: [Loading] -> [Success] or [Error]\n * @UX_FEEDBACK: User sees summary cards + risk block + JSON details after success.\n * @UX_RECOVERY: User can adjust selection and press Dry Run again.\n */\n async function startDryRun() {\n return belief_scope(\"startDryRun\", async () => {\n if (!sourceEnvId || !targetEnvId) {\n console.warn(\"[startDryRun][EXPLORE] Missing environment selection\");\n error =\n $t.migration?.select_both_envs ||\n \"Please select both source and target environments.\";\n return;\n }\n if (sourceEnvId === targetEnvId) {\n console.warn(\"[startDryRun][EXPLORE] Source and target environments are identical\");\n error =\n $t.migration?.different_envs ||\n \"Source and target environments must be different.\";\n return;\n }\n if (selectedDashboardIds.length === 0) {\n console.warn(\"[startDryRun][EXPLORE] No dashboards selected\");\n error =\n $t.migration?.select_dashboards ||\n \"Please select at least one dashboard to migrate.\";\n return;\n }\n\n console.info(\"[startDryRun][REASON] Initiating dry-run analysis\", { sourceEnvId, targetEnvId });\n error = \"\";\n dryRunLoading = true;\n try {\n const selection: DashboardSelection = {\n selected_ids: selectedDashboardIds,\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n replace_db_config: replaceDb,\n fix_cross_filters: fixCrossFilters,\n };\n dryRunResult = await api.postApi(\"/migration/dry-run\", selection);\n console.info(\"[startDryRun][REFLECT] Dry-run analysis completed\", { riskScore: dryRunResult.risk.score });\n } catch (e) {\n console.error(\"[startDryRun][EXPLORE] Dry-run analysis failed\", e);\n error = e.message;\n dryRunResult = null;\n } finally {\n dryRunLoading = false;\n }\n });\n }\n // #endregion startDryRun:Function\n" + "body": " // #region test_renders_and_fetches_envs [C:2] [TYPE Function]\n // @BRIEF Page fetches environments on mount.\n // @TEST_SCENARIO: environments_loaded_on_mount\n it('renders and fetches environments on mount', async () => {\n render(MigrationDashboard);\n expect(api.getEnvironmentsList).toHaveBeenCalled();\n });\n // #endregion test_renders_and_fetches_envs\n" + }, + { + "contract_id": "test_step1_next_disabled", + "contract_type": "Function", + "file_path": "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js", + "start_line": 120, + "end_line": 137, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Next button in step 1 is disabled without environment selection.", + "COMPLEXITY": 2, + "TEST_EDGE": "missing_field", + "TEST_SCENARIO": "step1_next_disabled_without_env" + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " // #region test_step1_next_disabled [C:2] [TYPE Function]\n // @BRIEF Next button in step 1 is disabled without environment selection.\n // @TEST_SCENARIO: step1_next_disabled_without_env\n // @TEST_EDGE: missing_field\n it('has step 1 shown by default with Next button disabled', async () => {\n const { getByRole, queryByText } = render(MigrationDashboard);\n\n // Step 1 is shown by default — find the Next button\n const nextBtn = getByRole('button', { name: /next/i });\n expect(nextBtn).toBeTruthy();\n // Next button should be disabled until environments selected\n expect(nextBtn.hasAttribute('disabled')).toBe(true);\n\n // Task History toggle should be present\n const taskHistoryBtn = queryByText(/task|последние задачи/i);\n expect(taskHistoryBtn).toBeTruthy();\n });\n // #endregion test_step1_next_disabled\n" + }, + { + "contract_id": "test_ui_has_header_and_toggle", + "contract_type": "Function", + "file_path": "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js", + "start_line": 156, + "end_line": 168, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Page header and task history toggle render correctly.", + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": " // #region test_ui_has_header_and_toggle [C:2] [TYPE Function]\n // @BRIEF Page header and task history toggle render correctly.\n it('renders page header and task history toggle button', async () => {\n const { getByText } = render(MigrationDashboard);\n\n // Header title should be present\n expect(getByText('Migration')).toBeTruthy();\n // Task history toggle button should exist\n const toggleBtn = getByText(/task|последние задачи/i);\n expect(toggleBtn).toBeTruthy();\n expect(toggleBtn.tagName).toBe('BUTTON');\n });\n // #endregion test_ui_has_header_and_toggle\n" }, { "contract_id": "MigrationMappingsPage", "contract_type": "Page", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", "start_line": 1, - "end_line": 233, + "end_line": 241, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -71588,14 +72932,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\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\n\n" + "body": "\n\n\n\n\n\n\n\n\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\n\n" }, { "contract_id": "MappingsPageScript:Block", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", "start_line": 37, - "end_line": 170, + "end_line": 178, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71624,14 +72968,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region MappingsPageScript:Block [TYPE Function]\n // @PURPOSE: Define imports, state, and handlers that drive migration mappings page FSM.\n // @RELATION CALLS ->[EXT:frontend:fetchEnvironments]\n // @RELATION CALLS ->[EXT:frontend:fetchDatabases]\n // @RELATION CALLS ->[EXT:frontend:handleUpdate]\n\n // #region Imports:Block [TYPE Function]\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 // #endregion Imports:Block\n\n // #region UiState:Store [TYPE Function]\n // @PURPOSE: Maintain local page state for environments, fetched databases, mappings, suggestions, and UX messages.\n let environments = [];\n let sourceEnvId = \"\";\n let targetEnvId = \"\";\n let sourceDatabases = [];\n let targetDatabases = [];\n let mappings = [];\n let suggestions = [];\n let loading = true;\n let fetchingDbs = false;\n let error = \"\";\n let success = \"\";\n // #endregion UiState:Store\n\n // #region belief_scope:Function [TYPE Function]\n // @PURPOSE: Frontend semantic scope wrapper for CRITICAL trace boundaries without changing business behavior.\n // @PRE: scopeId is non-empty and run is callable.\n // @POST: Executes run exactly once and returns/rejects with the same outcome.\n // @SIDE_EFFECT: Emits trace logs for semantic scope entrance/exit.\n // @DATA_CONTRACT: Input(scopeId:string, run:() => Promise) -> Output(Promise)\n async function belief_scope(scopeId: string, run: () => Promise): Promise {\n console.info(`[${scopeId}][REASON] belief_scope.enter`);\n try {\n return await run();\n } finally {\n console.info(`[${scopeId}][REFLECT] belief_scope.exit`);\n }\n }\n // #endregion belief_scope:Function\n\n // #region fetchEnvironments:Function [TYPE Function]\n // @PURPOSE: Load environment options for source/target selectors on initial mount.\n // @PRE: API client is initialized and route has mounted.\n // @POST: loading=false and environments populated on success or error message set on failure.\n // @SIDE_EFFECT: Network I/O to environments endpoint; mutates environments/error/loading.\n // @DATA_CONTRACT: Input(void) -> Output(EnvironmentSummary[])\n async function fetchEnvironments() {\n return belief_scope('migration.mappings.fetchEnvironments', async () => {\n try {\n environments = await api.getEnvironmentsList();\n } catch (e) {\n error = e.message;\n } finally {\n loading = false;\n }\n });\n }\n // #endregion fetchEnvironments:Function\n\n onMount(fetchEnvironments);\n\n // #region fetchDatabases:Function [TYPE Function]\n // @PURPOSE: Fetch both environment database catalogs, existing mappings, and suggested matches.\n // @PRE: sourceEnvId and targetEnvId are both selected and non-empty.\n // @POST: fetchingDbs=false and sourceDatabases/targetDatabases/mappings/suggestions updated or error set.\n // @SIDE_EFFECT: Concurrent network I/O to environments, mappings, and suggestion endpoints; clears transient messages.\n // @DATA_CONTRACT: Input({sourceEnvId,targetEnvId}) -> Output({sourceDatabases,targetDatabases,mappings,suggestions})\n async function fetchDatabases() {\n return belief_scope('migration.mappings.fetchDatabases', async () => {\n if (!sourceEnvId || !targetEnvId) return;\n fetchingDbs = true;\n error = \"\";\n success = \"\";\n \n try {\n const [src, tgt, maps, sugs] = await Promise.all([\n api.requestApi(`/environments/${sourceEnvId}/databases`),\n api.requestApi(`/environments/${targetEnvId}/databases`),\n api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),\n api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })\n ]);\n\n sourceDatabases = src;\n targetDatabases = tgt;\n mappings = maps;\n suggestions = sugs;\n } catch (e) {\n error = e.message;\n } finally {\n fetchingDbs = false;\n }\n });\n }\n // #endregion fetchDatabases:Function\n\n // #region handleUpdate:Function [TYPE Function]\n // @PURPOSE: Persist a selected mapping pair and reconcile local mapping list by source database UUID.\n // @PRE: event.detail includes sourceUuid/targetUuid and matching source/target database records exist.\n // @POST: mapping persisted; local mappings replaced for same source UUID; success or error feedback shown.\n // @SIDE_EFFECT: POST /mappings network I/O; mutates mappings/success/error.\n // @DATA_CONTRACT: Input(CustomEvent<{sourceUuid:string,targetUuid:string}>) -> Output(MappingRecord persisted + UI feedback)\n async function handleUpdate(event: CustomEvent) {\n return belief_scope('migration.mappings.handleUpdate', async () => {\n const { sourceUuid, targetUuid } = event.detail;\n const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);\n const tDb = targetDatabases.find(d => d.uuid === targetUuid);\n \n if (!sDb || !tDb) return;\n\n try {\n const savedMapping = await api.postApi('/mappings', {\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n source_db_uuid: sourceUuid,\n target_db_uuid: targetUuid,\n source_db_name: sDb.database_name,\n target_db_name: tDb.database_name\n });\n\n mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];\n success = $t.migration?.mapping_saved ;\n } catch (e) {\n error = e.message;\n }\n });\n }\n // #endregion handleUpdate:Function\n // #endregion MappingsPageScript:Block\n" + "body": " // #region MappingsPageScript:Block [TYPE Function]\n // @PURPOSE: Define imports, state, and handlers that drive migration mappings page FSM.\n // @RELATION CALLS ->[EXT:frontend:fetchEnvironments]\n // @RELATION CALLS ->[EXT:frontend:fetchDatabases]\n // @RELATION CALLS ->[EXT:frontend:handleUpdate]\n\n // #region Imports:Block [TYPE Function]\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 import { fromStore } from 'svelte/store';\n import { environmentContextStore } from '$lib/stores/environmentContext.js';\n // #endregion Imports:Block\n\n // #region UiState:Store [TYPE Function]\n // @PURPOSE: Maintain local page state for environments, fetched databases, mappings, suggestions, and UX messages.\n let environments = [];\n let sourceEnvId = \"\";\n let targetEnvId = \"\";\n const envContextState = fromStore(environmentContextStore);\n let sourceDatabases = [];\n let targetDatabases = [];\n let mappings = [];\n let suggestions = [];\n let loading = true;\n let fetchingDbs = false;\n let error = \"\";\n let success = \"\";\n // #endregion UiState:Store\n\n // #region belief_scope:Function [TYPE Function]\n // @PURPOSE: Frontend semantic scope wrapper for CRITICAL trace boundaries without changing business behavior.\n // @PRE: scopeId is non-empty and run is callable.\n // @POST: Executes run exactly once and returns/rejects with the same outcome.\n // @SIDE_EFFECT: Emits trace logs for semantic scope entrance/exit.\n // @DATA_CONTRACT: Input(scopeId:string, run:() => Promise) -> Output(Promise)\n async function belief_scope(scopeId: string, run: () => Promise): Promise {\n console.info(`[${scopeId}][REASON] belief_scope.enter`);\n try {\n return await run();\n } finally {\n console.info(`[${scopeId}][REFLECT] belief_scope.exit`);\n }\n }\n // #endregion belief_scope:Function\n\n // #region fetchEnvironments:Function [TYPE Function]\n // @PURPOSE: Load environment options for source/target selectors on initial mount.\n // @PRE: API client is initialized and route has mounted.\n // @POST: loading=false and environments populated on success or error message set on failure.\n // @SIDE_EFFECT: Network I/O to environments endpoint; mutates environments/error/loading.\n // @DATA_CONTRACT: Input(void) -> Output(EnvironmentSummary[])\n async function fetchEnvironments() {\n return belief_scope('migration.mappings.fetchEnvironments', async () => {\n try {\n environments = await api.getEnvironmentsList();\n // Pre-fill source with active environment, leave target empty\n const activeEnvId = envContextState.current?.selectedEnvId;\n if (activeEnvId && environments.some((env) => env.id === activeEnvId)) {\n sourceEnvId = activeEnvId;\n }\n } catch (e) {\n error = e.message;\n } finally {\n loading = false;\n }\n });\n }\n // #endregion fetchEnvironments:Function\n\n onMount(fetchEnvironments);\n\n // #region fetchDatabases:Function [TYPE Function]\n // @PURPOSE: Fetch both environment database catalogs, existing mappings, and suggested matches.\n // @PRE: sourceEnvId and targetEnvId are both selected and non-empty.\n // @POST: fetchingDbs=false and sourceDatabases/targetDatabases/mappings/suggestions updated or error set.\n // @SIDE_EFFECT: Concurrent network I/O to environments, mappings, and suggestion endpoints; clears transient messages.\n // @DATA_CONTRACT: Input({sourceEnvId,targetEnvId}) -> Output({sourceDatabases,targetDatabases,mappings,suggestions})\n async function fetchDatabases() {\n return belief_scope('migration.mappings.fetchDatabases', async () => {\n if (!sourceEnvId || !targetEnvId) return;\n fetchingDbs = true;\n error = \"\";\n success = \"\";\n \n try {\n const [src, tgt, maps, sugs] = await Promise.all([\n api.requestApi(`/environments/${sourceEnvId}/databases`),\n api.requestApi(`/environments/${targetEnvId}/databases`),\n api.requestApi(`/mappings?source_env_id=${sourceEnvId}&target_env_id=${targetEnvId}`),\n api.postApi(`/mappings/suggest`, { source_env_id: sourceEnvId, target_env_id: targetEnvId })\n ]);\n\n sourceDatabases = src;\n targetDatabases = tgt;\n mappings = maps;\n suggestions = sugs;\n } catch (e) {\n error = e.message;\n } finally {\n fetchingDbs = false;\n }\n });\n }\n // #endregion fetchDatabases:Function\n\n // #region handleUpdate:Function [TYPE Function]\n // @PURPOSE: Persist a selected mapping pair and reconcile local mapping list by source database UUID.\n // @PRE: event.detail includes sourceUuid/targetUuid and matching source/target database records exist.\n // @POST: mapping persisted; local mappings replaced for same source UUID; success or error feedback shown.\n // @SIDE_EFFECT: POST /mappings network I/O; mutates mappings/success/error.\n // @DATA_CONTRACT: Input(CustomEvent<{sourceUuid:string,targetUuid:string}>) -> Output(MappingRecord persisted + UI feedback)\n async function handleUpdate(event: CustomEvent) {\n return belief_scope('migration.mappings.handleUpdate', async () => {\n const { sourceUuid, targetUuid } = event.detail;\n const sDb = sourceDatabases.find(d => d.uuid === sourceUuid);\n const tDb = targetDatabases.find(d => d.uuid === targetUuid);\n \n if (!sDb || !tDb) return;\n\n try {\n const savedMapping = await api.postApi('/mappings', {\n source_env_id: sourceEnvId,\n target_env_id: targetEnvId,\n source_db_uuid: sourceUuid,\n target_db_uuid: targetUuid,\n source_db_name: sDb.database_name,\n target_db_name: tDb.database_name\n });\n\n mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];\n success = $t.migration?.mapping_saved ;\n } catch (e) {\n error = e.message;\n }\n });\n }\n // #endregion handleUpdate:Function\n // #endregion MappingsPageScript:Block\n" }, { "contract_id": "Imports:Block", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", "start_line": 43, - "end_line": 50, + "end_line": 52, "tier": "TIER_1", "complexity": 1, "metadata": {}, @@ -71639,14 +72983,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region Imports:Block [TYPE Function]\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 // #endregion Imports:Block\n" + "body": " // #region Imports:Block [TYPE Function]\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 import { fromStore } from 'svelte/store';\n import { environmentContextStore } from '$lib/stores/environmentContext.js';\n // #endregion Imports:Block\n" }, { "contract_id": "UiState:Store", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", - "start_line": 52, - "end_line": 65, + "start_line": 54, + "end_line": 68, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71656,14 +73000,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region UiState:Store [TYPE Function]\n // @PURPOSE: Maintain local page state for environments, fetched databases, mappings, suggestions, and UX messages.\n let environments = [];\n let sourceEnvId = \"\";\n let targetEnvId = \"\";\n let sourceDatabases = [];\n let targetDatabases = [];\n let mappings = [];\n let suggestions = [];\n let loading = true;\n let fetchingDbs = false;\n let error = \"\";\n let success = \"\";\n // #endregion UiState:Store\n" + "body": " // #region UiState:Store [TYPE Function]\n // @PURPOSE: Maintain local page state for environments, fetched databases, mappings, suggestions, and UX messages.\n let environments = [];\n let sourceEnvId = \"\";\n let targetEnvId = \"\";\n const envContextState = fromStore(environmentContextStore);\n let sourceDatabases = [];\n let targetDatabases = [];\n let mappings = [];\n let suggestions = [];\n let loading = true;\n let fetchingDbs = false;\n let error = \"\";\n let success = \"\";\n // #endregion UiState:Store\n" }, { "contract_id": "belief_scope:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", - "start_line": 67, - "end_line": 81, + "start_line": 70, + "end_line": 84, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71683,8 +73027,8 @@ "contract_id": "fetchDatabases:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", - "start_line": 104, - "end_line": 136, + "start_line": 112, + "end_line": 144, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71704,8 +73048,8 @@ "contract_id": "handleUpdate:Function", "contract_type": "Function", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", - "start_line": 138, - "end_line": 169, + "start_line": 146, + "end_line": 177, "tier": "TIER_1", "complexity": 1, "metadata": { @@ -71725,8 +73069,8 @@ "contract_id": "MappingsPageTemplate", "contract_type": "Block", "file_path": "frontend/src/routes/migration/mappings/+page.svelte", - "start_line": 173, - "end_line": 231, + "start_line": 181, + "end_line": 239, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -72860,7 +74204,7 @@ "contract_type": "Page", "file_path": "frontend/src/routes/tools/mapper/+page.svelte", "start_line": 1, - "end_line": 36, + "end_line": 45, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -72910,7 +74254,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n
\n \n \n
\n
\n \n
\n
\n \n
\n
\n
\n\n" + "body": "\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
\n
\n\n" }, { "contract_id": "loadFiles:Function", @@ -73051,23 +74395,18 @@ "contract_type": "Page", "file_path": "frontend/src/routes/translate/[id]/+page.svelte", "start_line": 1, - "end_line": 1476, + "end_line": 779, "tier": "TIER_2", "complexity": 4, "metadata": { - "BRIEF": "Translation job configuration page - orchestrates form state, datasource loading, LLM settings, run/schedule tabs.", - "COMPLEXITY": 4, - "DEPRECATED": "N/A — active page.", - "RATIONALE": "Kept as C4 orchestration due to complex lifecycle: load→configure→validate→save→run→monitor across multiple tabs and API interactions.", - "REJECTED": "Extracting all sub-tabs into separate page components was rejected due to deep state coupling between datasource selection, column loading, and form validation — would require a global store or excessive prop drilling.", - "REPLACED_BY": "N/A — no replacement.", - "TYPE": "{string} idle | loading | configured | saving | validation_error | datasource_unavailable" + "BRIEF": "Translation job configuration page — 5-step wizard with logical separation:", + "COMPLEXITY": 4 }, "relations": [], "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

\n {isNewJob ? $t.translate?.config?.new_title : $t.translate?.config?.edit_title}\n

\n {#if !isNewJob && existingJob}\n

{$t.translate?.config?.id_label.replace('{id}', existingJob.id)}

\n {/if}\n
\n
\n goto('/translate')}\n class=\"px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors\"\n >\n {$t.translate?.config?.cancel}\n \n \n {isSaving ? $t.translate?.config?.saving : $t.translate?.config?.save}\n \n
\n
\n\n \n {#if warnings.length > 0}\n
\n

{$t.translate?.config?.warnings_title}

\n
    \n {#each warnings as w}\n
  • {w}
  • \n {/each}\n
\n
\n {/if}\n\n \n {#if uxState === 'loading'}\n
\n {#each Array(6) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.translate?.common?.unknown_error}

\n goto('/translate')}\n class=\"px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors\"\n >\n {$t.translate?.config?.back_to_jobs}\n \n
\n\n \n {:else if uxState === 'datasource_unavailable'}\n
\n

\n {$t.translate?.config?.datasource_unavailable}\n

\n
\n\n \n {:else if uxState === 'configured' || uxState === 'saving' || uxState === 'validation_error' || uxState === 'idle'}\n \n
\n \n
\n\n \n
\n
\n \n
\n

{$t.translate?.config?.basic_info}

\n
\n
\n \n \n {#if validationErrors.name}\n

{validationErrors.name}

\n {/if}\n
\n
\n \n \n
\n
\n
\n\n \n
\n

{$t.translate?.config?.datasource}

\n
\n
\n \n \n \n {#each environments as env}\n \n {/each}\n \n
\n
\n \n searchDatasources('')}\n onblur={() => setTimeout(() => (showDatasourceDropdown = false), 150)}\n class=\"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring\"\n placeholder={$t.translate?.config?.datasource_search_placeholder || 'Search datasets...'}\n />\n {#if datasourceLoading}\n
\n {/if}\n {#if showDatasourceDropdown}\n
\n {#each datasourceList as ds}\n selectDatasource(ds)}\n class=\"w-full text-left px-3 py-2 text-sm hover:bg-blue-50 border-b border-gray-100 last:border-b-0\"\n >\n {ds.table_name}\n {ds.schema}\n {ds.database_name} · {ds.database_dialect}\n \n {/each}\n
\n {/if}\n
\n {#if databaseDialect}\n
\n \n {$t.translate?.config?.detected_dialect.replace('{dialect}', databaseDialect)}\n \n
\n {/if}\n
\n\n {#if isColumnsLoading}\n
\n {/if}\n\n {#if columnList.length > 0}\n
\n \n
\n {#each columnList as col}\n
\n {col.name}\n {#if col.type}\n ({col.type})\n {/if}\n {#if isVirtual(col.name)}\n {$t.translate?.config?.virtual}\n {/if}\n
\n {/each}\n
\n
\n {/if}\n
\n\n \n
\n

{$t.translate?.config?.column_mapping}

\n\n \n
\n \n \n \n {#each columnList as col}\n \n {/each}\n \n {#if validationErrors.translationColumn}\n

{validationErrors.translationColumn}

\n {/if}\n
\n\n \n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n
\n
\n \n {#if sourceKeyCols.length > 0}\n { sourceKeyCols = []; targetKeyCols = []; }}\n class=\"text-xs text-red-600 hover:text-red-700\"\n >\n {$t.translate?.config?.clear_all}\n \n {/if}\n
\n\n {#if columnList.length === 0}\n
\n {\n sourceKeyCols = [...sourceKeyCols, ''];\n targetKeyCols = [...targetKeyCols, ''];\n }}\n class=\"px-3 py-1 text-xs border border-dashed border-gray-300 rounded text-gray-500 hover:border-primary-ring hover:text-primary\"\n >\n {$t.translate?.config?.add_key_column}\n \n
\n {:else}\n
\n {#each columnList as col}\n toggleSourceKeyCol(col.name)}\n class=\"px-2.5 py-1 text-xs rounded-full border transition-colors\n {sourceKeyCols.includes(col.name)\n ? 'bg-blue-100 border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n title={isVirtual(col.name) ? $t.translate?.config?.virtual_column_warning.replace('{col}', col.name) : ''}\n >\n {col.name}\n {#if isVirtual(col.name)}\n *\n {/if}\n \n {/each}\n
\n {/if}\n\n {#if sourceKeyCols.length > 0}\n
\n {#each sourceKeyCols as srcCol, idx}\n
\n \n \n updateTargetKeyCol(idx, e.target.value)}\n class=\"flex-1 px-3 py-1.5 border border-gray-300 rounded text-sm font-mono focus-visible:ring-2 focus-visible:ring-primary-ring\"\n />\n {\n sourceKeyCols = sourceKeyCols.filter((_, i) => i !== idx);\n targetKeyCols = targetKeyCols.filter((_, i) => i !== idx);\n }}\n class=\"p-1 text-gray-400 hover:text-red-500\"\n >\n \n \n \n \n
\n {/each}\n
\n {/if}\n {#if validationErrors.targetKeyCols}\n

{validationErrors.targetKeyCols}

\n {/if}\n
\n\n \n
\n \n {#if columnList.length === 0}\n

{$t.translate?.config?.select_datasource_hint}

\n {:else}\n
\n {#each columnList as col}\n toggleContextColumn(col.name)}\n class=\"px-2.5 py-1 text-xs rounded-full border transition-colors\n {contextColumns.includes(col.name)\n ? 'bg-purple-100 border-purple-300 text-purple-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {col.name}\n {#if isVirtual(col.name)}\n *\n {/if}\n \n {/each}\n
\n {/if}\n
\n
\n\n \n
\n

{$t.translate?.config?.target_table_title}

\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n \n {#each databases as db}\n \n {/each}\n \n {#if databasesLoading}\n

{$t.translate?.config?.loading_databases}

\n {:else if !environmentId}\n

{$t.translate?.config?.select_environment}

\n {:else}\n

{$t.translate?.config?.target_database_hint}

\n {/if}\n
\n
\n\n \n
\n

{$t.translate?.config?.target_column_mapping_title || 'Target Column Mapping'}

\n

{$t.translate?.config?.target_column_mapping_description || 'Configure which columns receive translated data, language codes, source text, and detected source language during INSERT.'}

\n
\n
\n \n \n

{$t.translate?.config?.target_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_language_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_source_column_hint}

\n
\n
\n \n \n

{$t.translate?.config?.target_source_language_column_hint}

\n
\n
\n
\n\n \n \n
\n\n \n
\n

{$t.translate?.config?.llm_settings}

\n
\n
\n \n \n
\n
\n \n {#if targetLanguages.length === 0}\n

{$t.translate?.config?.target_language_required || 'Select at least one target language'}

\n {/if}\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n
\n \n
\n\n \n
\n \n
\n
\n\n \n
\n

{$t.translate?.config?.terminology_dictionaries}

\n {#if availableDictionaries.length === 0}\n

\n {$t.translate?.config?.no_dictionaries}\n

\n {:else}\n
\n {#each availableDictionaries as dict}\n \n {/each}\n
\n {/if}\n
\n
\n\n
\n\n \n {#if !isNewJob}\n
\n
\n \n
\n
\n {/if}\n\n \n
\n {#if !isNewJob && existingJob}\n {#if !configValid}\n
\n

\n {$t.translate?.preview?.config_incomplete}\n

\n

\n {$t.translate?.preview?.config_requirements}\n

\n
\n {:else}\n
\n

{$t.translate?.preview?.title}

\n \n
\n {/if}\n {:else}\n
\n

{$t.translate?.config?.save_job_first || 'Save the job first'}

\n
\n {/if}\n
\n\n \n
\n {#if !isNewJob && existingJob}\n
\n

{$t.translate?.config?.run_translation}

\n\n \n
\n {$t.translate?.config?.status}:\n \n {getJobStatusLabel(status)}\n \n {#if status === 'DRAFT'}\n {\n try {\n await updateJob(jobId, { status: 'READY' });\n status = 'READY';\n addToast($t.translate?.config?.job_updated, 'success');\n } catch (e) {\n addToast(e?.message || 'Failed to update status', 'error');\n }\n }}\n class=\"ml-auto px-3 py-1 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors\"\n >\n {$t.translate?.config?.mark_ready || 'Mark as READY'}\n \n {/if}\n
\n\n
\n \n
\n \n
\n
\n
\n \n \n \n
\n
\n

{$t.translate?.config?.run_incremental}

\n

{$t.translate?.config?.run_incremental_desc}

\n handleTriggerRun(false)}\n disabled={isRunning || status === 'DRAFT'}\n class=\"mt-3 px-5 py-1.5 text-sm font-medium bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isRunning && !isFullRun ? $t.translate?.config?.running : $t.translate?.config?.run_translation}\n \n
\n
\n
\n\n \n
\n
\n
\n \n \n \n
\n
\n

{$t.translate?.config?.run_full}

\n

{$t.translate?.config?.run_full_desc}

\n handleTriggerRun(true)}\n disabled={isRunning || status === 'DRAFT'}\n class=\"mt-3 px-5 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isRunning && isFullRun ? $t.translate?.config?.running : $t.translate?.config?.full_translate}\n \n
\n
\n
\n
\n\n {#if runError}\n
\n

{runError}

\n
\n {/if}\n\n {#if currentRunId && !runComplete}\n \n {/if}\n\n {#if completedRuns.length > 0}\n
\n
\n

{$t.translate?.config?.recent_runs}

\n showPageBulkReplace = true}\n class=\"px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors\"\n >\n {$t.translate?.run?.bulk_replace || 'Bulk Replace'}\n \n
\n
\n {#each completedRuns as run}\n
\n
\n
\n
\n {$t.translate?.run?.run_id}\n {run.id}\n \n {run.status}\n \n
\n
\n {$t.translate?.run?.total}: {run.total_records || 0}\n {$t.translate?.run?.success}: {run.successful_records || 0}\n {$t.translate?.run?.failed}: {run.failed_records || 0}\n {$t.translate?.run?.skipped}: {run.skipped_records || 0}\n
\n {#if run.error_message}\n

{run.error_message}

\n {/if}\n
\n toggleRunDetails(run.id)}\n class=\"shrink-0 px-3 py-1.5 text-xs border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors\"\n >\n {expandedRunIds.includes(run.id)\n ? ($t.translate?.run?.hide_details || 'Hide details')\n : ($t.translate?.run?.show_details || 'Show details')}\n \n
\n {#if expandedRunIds.includes(run.id)}\n
\n \n
\n {/if}\n
\n {/each}\n
\n
\n {/if}\n
\n
\n {:else}\n
\n

{$t.translate?.config?.save_job_first || 'Save the job first'}

\n
\n {/if}\n
\n {/if}\n
\n\n showPageBulkReplace = false}\n\tonApplied={(count) => { showPageBulkReplace = false; loadRunHistory(); }}\n/>\n\n" + "body": "\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\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\n\n\n\n\n\n\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

\n {isNewJob ? $t.translate?.config?.new_title : $t.translate?.config?.edit_title}\n

\n {#if !isNewJob && existingJob}\n

{$t.translate?.config?.id_label.replace('{id}', existingJob.id)}

\n {/if}\n
\n
\n goto('/translate')}\n class=\"px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors\"\n >\n {$t.translate?.config?.cancel}\n \n \n {isSaving ? $t.translate?.config?.saving : $t.translate?.config?.save}\n \n
\n
\n\n \n {#if warnings.length > 0}\n
\n

{$t.translate?.config?.warnings_title}

\n
    \n {#each warnings as w}\n
  • {w}
  • \n {/each}\n
\n
\n {/if}\n\n \n {#if uxState === 'loading'}\n
\n {#each Array(6) as _}\n
\n {/each}\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.translate?.common?.unknown_error}

\n goto('/translate')}\n class=\"px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors\"\n >\n {$t.translate?.config?.back_to_jobs}\n \n
\n\n \n {:else if uxState === 'datasource_unavailable'}\n
\n

\n {$t.translate?.config?.datasource_unavailable}\n

\n
\n\n \n {:else if uxState === 'configured' || uxState === 'saving' || uxState === 'validation_error' || uxState === 'idle'}\n \n
\n \n
\n\n \n
\n \n
\n\n \n {#if !isNewJob}\n
\n
\n \n
\n
\n {/if}\n\n \n
\n {#if !isNewJob && existingJob}\n {#if !configValid}\n
\n

\n {$t.translate?.preview?.config_incomplete}\n

\n

\n {$t.translate?.preview?.config_requirements}\n

\n
\n {:else}\n \n {/if}\n {:else}\n
\n

{$t.translate?.config?.save_job_first || 'Save the job first'}

\n
\n {/if}\n
\n\n \n
\n {#if !isNewJob && existingJob}\n \n {:else}\n
\n

{$t.translate?.config?.save_job_first || 'Save the job first'}

\n
\n {/if}\n
\n\n \n
\n {#if !isNewJob && existingJob}\n { showPageBulkReplace = false; loadRunHistory(); }}\n onBulkReplaceClose={() => showPageBulkReplace = false}\n {getJobStatusLabel}\n />\n {:else}\n
\n

{$t.translate?.config?.save_job_first || 'Save the job first'}

\n
\n {/if}\n
\n {/if}\n
\n\n showPageBulkReplace = false}\n\tonApplied={(count) => { showPageBulkReplace = false; loadRunHistory(); }}\n/>\n\n" }, { "contract_id": "DictionariesPage", @@ -73149,7 +74488,7 @@ "contract_type": "Page", "file_path": "frontend/src/routes/validation/[id]/+page.svelte", "start_line": 1, - "end_line": 722, + "end_line": 731, "tier": "TIER_2", "complexity": 4, "metadata": { @@ -73167,7 +74506,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

\n {isNewTask ? ($t.validation?.new_task || 'New Validation Task') : ($t.validation?.edit_task || 'Edit Validation Task')}\n

\n {#if !isNewTask && task}\n

{$t.validation?.created_at || 'Created'}: {task?.created_at ? new Date(task.created_at).toLocaleString() : '-'}

\n {/if}\n
\n
\n {#if !isNewTask}\n \n {#if isRunning}\n \n \n \n \n {$t.validation?.running || 'Running...'}\n {:else}\n {$t.validation?.run_now || 'Run Now'}\n {/if}\n \n {/if}\n \n {$t.validation?.cancel || 'Cancel'}\n \n \n {isSaving ? ($t.validation?.saving || 'Saving...') : ($t.validation?.save || 'Save')}\n \n
\n
\n\n \n {#if uxState === 'loading'}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.validation?.load_task_failed || 'Failed to load task.'}

\n
\n \n {$t.validation?.retry || 'Retry'}\n \n \n {$t.validation?.back || 'Back'}\n \n
\n
\n\n \n {:else if uxState === 'not_found'}\n
\n

{$t.validation?.task_not_found || 'Task not found'}

\n

{$t.validation?.task_not_found_desc || 'The task you are looking for does not exist or has been deleted.'}

\n \n {$t.validation?.go_back || 'Go back'}\n \n
\n\n \n {:else if uxState === 'configured' || uxState === 'saving'}\n \n
\n \n
\n\n \n {#if !isNewTask && providerId && !isProviderMultimodal}\n
\n

\n \n \n \n {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider (supports images).'}\n

\n
\n {/if}\n\n \n {#if activeTab === 'config'}\n
\n \n
\n

{$t.validation?.task_name || 'Basic Info'}

\n
\n
\n \n \n {#if validationErrors.name}\n

{validationErrors.name}

\n {/if}\n
\n
\n
\n\n \n
\n

{$t.validation?.dashboard || 'Dashboard & Environment'}

\n
\n
\n \n { dashboardIds = []; searchDashboards(''); }}\n class=\"w-full px-3 py-2 border rounded-lg text-sm {validationErrors.environment_id ? 'border-red-300 bg-red-50' : 'border-gray-300'}\"\n >\n \n {#each environments as env (env.id)}\n \n {/each}\n \n {#if validationErrors.environment_id}\n

{validationErrors.environment_id}

\n {/if}\n
\n
\n \n ({ id: String(d.id), name: d.title || d.dashboard_title || d.name || String(d.id), subtitle: d.schema || '' }))}\n bind:selected={dashboardIds}\n placeholder={$t.validation?.dashboard || 'Select dashboard...'}\n selectAllLabel={$t.dashboard?.select_all || 'Select All'}\n clearLabel=\"Clear\"\n emptyMessage=\"No dashboards found\"\n disabled={!environmentId}\n />\n {#if validationErrors.dashboard_id}\n

{validationErrors.dashboard_id}

\n {/if}\n {#if dashboardsLoading}\n

Loading...

\n {/if}\n
\n
\n
\n\n \n
\n

{$t.validation?.llm_provider || 'LLM Provider'}

\n
\n \n \n \n {#each multimodalProviders as p (p.id)}\n \n {/each}\n \n {#if validationErrors.provider_id}\n

{validationErrors.provider_id}

\n {/if}\n {#if providerId && !isProviderMultimodal}\n

\n \n \n \n {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider.'}\n

\n {/if}\n
\n
\n\n \n
\n
\n

{$t.validation?.schedule || 'Schedule'}

\n (showSchedule = !showSchedule)}\n class=\"text-sm text-blue-600 hover:text-blue-700 font-medium\"\n >\n {showSchedule\n ? ($t.validation?.schedule_collapse || 'Hide schedule')\n : ($t.validation?.schedule_expand || 'Configure schedule')}\n \n
\n\n {#if !showSchedule}\n

{$t.validation?.manual_only || 'Manual only — runs triggered by user.'}

\n {:else}\n
\n
\n \n \n
\n {#each DAY_LABELS as day, idx (day)}\n toggleDay(idx)}\n class=\"px-3 py-1.5 text-sm rounded-lg border transition-colors\n {scheduleDays.includes(idx)\n ? 'bg-blue-100 border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {$t.validation?.[DAY_KEYS[idx]] || day}\n \n {/each}\n
\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n

\n \n \n \n {$t.validation?.schedule_timezone_hint || 'All times in'} {appTimezone}\n

\n
\n {/if}\n
\n\n \n
\n \n
\n
\n\n \n {:else if activeTab === 'history'}\n
\n \n
\n \n \n \n
\n

{$t.validation?.history_info_title || 'About run history'}

\n

{$t.validation?.history_info_desc || 'This tab shows validation runs for this specific task. Runs appear after you click \"Run Now\" or when the scheduled task executes. Each run captures a dashboard screenshot and sends it to the LLM provider for analysis.'}

\n
\n
\n\n {#if runsLoading}\n
\n {#each Array(3) as _, i (i)}\n
\n {/each}\n
\n {:else if runs.length === 0}\n
\n

{$t.validation?.run_history_empty || 'No runs for this task yet.'}

\n

{$t.validation?.run_history_empty_hint || 'Click \"Run Now\" above to start a validation run. The result will appear here once analysis completes.'}

\n
\n {:else}\n
\n \n \n \n \n \n \n \n \n \n \n \n {#each runs as run (run.id)}\n \n \n \n \n \n \n \n {/each}\n \n
{$t.validation?.status || 'Status'}{$t.validation?.summary || 'Summary'}{$t.validation?.issues || 'Issues'}{$t.validation?.timestamp || 'Timestamp'}{$t.validation?.actions || 'Actions'}
\n \n {run.status || 'UNKNOWN'}\n \n \n {run.summary || '-'}\n \n {run.issues_count ?? run.issues ?? '-'}\n \n {run.timestamp ? new Date(run.timestamp).toLocaleString() : '-'}\n \n handleNavToReport(run.task_id)}\n class=\"text-blue-600 hover:text-blue-700 text-sm font-medium\"\n >\n {$t.validation?.view_report || 'View Report'}\n \n
\n
\n\n \n {#if runsTotal > runsPageSize}\n
\n

\n {($t.validation?.showing || 'Showing {count} of {total}')\n .replace('{count}', String(runs.length))\n .replace('{total}', String(runsTotal))}\n

\n
\n { runsPage--; loadRunHistory(); }}\n disabled={runsPage <= 1}\n class=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50 hover:bg-gray-50\"\n >\n {$t.validation?.previous || 'Previous'}\n \n { runsPage++; loadRunHistory(); }}\n disabled={runsPage * runsPageSize >= runsTotal}\n class=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50 hover:bg-gray-50\"\n >\n {$t.validation?.next || 'Next'}\n \n
\n
\n {/if}\n {/if}\n
\n {/if}\n {/if}\n
\n\n" + "body": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n {pageTitle}\n\n\n
\n \n
\n
\n

\n {isNewTask ? ($t.validation?.new_task || 'New Validation Task') : ($t.validation?.edit_task || 'Edit Validation Task')}\n

\n {#if !isNewTask && task}\n

{$t.validation?.created_at || 'Created'}: {task?.created_at ? new Date(task.created_at).toLocaleString() : '-'}

\n {/if}\n
\n
\n {#if !isNewTask}\n \n {#if isRunning}\n \n \n \n \n {$t.validation?.running || 'Running...'}\n {:else}\n {$t.validation?.run_now || 'Run Now'}\n {/if}\n \n {/if}\n \n {$t.validation?.cancel || 'Cancel'}\n \n \n {isSaving ? ($t.validation?.saving || 'Saving...') : ($t.validation?.save || 'Save')}\n \n
\n
\n\n \n {#if uxState === 'loading'}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n \n {:else if uxState === 'error'}\n
\n

{error || $t.validation?.load_task_failed || 'Failed to load task.'}

\n
\n \n {$t.validation?.retry || 'Retry'}\n \n \n {$t.validation?.back || 'Back'}\n \n
\n
\n\n \n {:else if uxState === 'not_found'}\n
\n

{$t.validation?.task_not_found || 'Task not found'}

\n

{$t.validation?.task_not_found_desc || 'The task you are looking for does not exist or has been deleted.'}

\n \n {$t.validation?.go_back || 'Go back'}\n \n
\n\n \n {:else if uxState === 'configured' || uxState === 'saving'}\n \n
\n \n
\n\n \n {#if !isNewTask && providerId && !isProviderMultimodal}\n
\n

\n \n \n \n {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider (supports images).'}\n

\n
\n {/if}\n\n \n {#if activeTab === 'config'}\n
\n \n
\n

{$t.validation?.task_name || 'Basic Info'}

\n
\n
\n \n \n {#if validationErrors.name}\n

{validationErrors.name}

\n {/if}\n
\n
\n
\n\n \n
\n

{$t.validation?.dashboard || 'Dashboard & Environment'}

\n
\n
\n \n { dashboardIds = []; searchDashboards(''); }}\n class=\"w-full px-3 py-2 border rounded-lg text-sm {validationErrors.environment_id ? 'border-red-300 bg-red-50' : 'border-gray-300'}\"\n >\n \n {#each environments as env (env.id)}\n \n {/each}\n \n {#if validationErrors.environment_id}\n

{validationErrors.environment_id}

\n {/if}\n
\n
\n \n ({ id: String(d.id), name: d.title || d.dashboard_title || d.name || String(d.id), subtitle: d.schema || '' }))}\n bind:selected={dashboardIds}\n placeholder={$t.validation?.dashboard || 'Select dashboard...'}\n selectAllLabel={$t.dashboard?.select_all || 'Select All'}\n clearLabel=\"Clear\"\n emptyMessage=\"No dashboards found\"\n disabled={!environmentId}\n />\n {#if validationErrors.dashboard_id}\n

{validationErrors.dashboard_id}

\n {/if}\n {#if dashboardsLoading}\n

Loading...

\n {/if}\n
\n
\n
\n\n \n
\n

{$t.validation?.llm_provider || 'LLM Provider'}

\n
\n \n \n \n {#each multimodalProviders as p (p.id)}\n \n {/each}\n \n {#if validationErrors.provider_id}\n

{validationErrors.provider_id}

\n {/if}\n {#if providerId && !isProviderMultimodal}\n

\n \n \n \n {$t.validation?.provider_multimodal_required || 'Dashboard validation requires a multimodal LLM provider.'}\n

\n {/if}\n
\n
\n\n \n
\n
\n

{$t.validation?.schedule || 'Schedule'}

\n (showSchedule = !showSchedule)}\n class=\"text-sm text-blue-600 hover:text-blue-700 font-medium\"\n >\n {showSchedule\n ? ($t.validation?.schedule_collapse || 'Hide schedule')\n : ($t.validation?.schedule_expand || 'Configure schedule')}\n \n
\n\n {#if !showSchedule}\n

{$t.validation?.manual_only || 'Manual only — runs triggered by user.'}

\n {:else}\n
\n
\n \n \n
\n {#each DAY_LABELS as day, idx (day)}\n toggleDay(idx)}\n class=\"px-3 py-1.5 text-sm rounded-lg border transition-colors\n {scheduleDays.includes(idx)\n ? 'bg-blue-100 border-blue-300 text-blue-700'\n : 'bg-white border-gray-200 text-gray-600 hover:border-gray-300'}\"\n >\n {$t.validation?.[DAY_KEYS[idx]] || day}\n \n {/each}\n
\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n \n

\n \n \n \n {$t.validation?.schedule_timezone_hint || 'All times in'} {appTimezone}\n

\n
\n {/if}\n
\n\n \n
\n \n
\n
\n\n \n {:else if activeTab === 'history'}\n
\n \n
\n \n \n \n
\n

{$t.validation?.history_info_title || 'About run history'}

\n

{$t.validation?.history_info_desc || 'This tab shows validation runs for this specific task. Runs appear after you click \"Run Now\" or when the scheduled task executes. Each run captures a dashboard screenshot and sends it to the LLM provider for analysis.'}

\n
\n
\n\n {#if runsLoading}\n
\n {#each Array(3) as _, i (i)}\n
\n {/each}\n
\n {:else if runs.length === 0}\n
\n

{$t.validation?.run_history_empty || 'No runs for this task yet.'}

\n

{$t.validation?.run_history_empty_hint || 'Click \"Run Now\" above to start a validation run. The result will appear here once analysis completes.'}

\n
\n {:else}\n
\n \n \n \n \n \n \n \n \n \n \n \n {#each runs as run (run.id)}\n \n \n \n \n \n \n \n {/each}\n \n
{$t.validation?.status || 'Status'}{$t.validation?.summary || 'Summary'}{$t.validation?.issues || 'Issues'}{$t.validation?.timestamp || 'Timestamp'}{$t.validation?.actions || 'Actions'}
\n \n {run.status || 'UNKNOWN'}\n \n \n {run.summary || '-'}\n \n {run.issues_count ?? run.issues ?? '-'}\n \n {run.timestamp ? new Date(run.timestamp).toLocaleString() : '-'}\n \n handleNavToReport(run.task_id)}\n class=\"text-blue-600 hover:text-blue-700 text-sm font-medium\"\n >\n {$t.validation?.view_report || 'View Report'}\n \n
\n
\n\n \n {#if runsTotal > runsPageSize}\n
\n

\n {($t.validation?.showing || 'Showing {count} of {total}')\n .replace('{count}', String(runs.length))\n .replace('{total}', String(runsTotal))}\n

\n
\n { runsPage--; loadRunHistory(); }}\n disabled={runsPage <= 1}\n class=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50 hover:bg-gray-50\"\n >\n {$t.validation?.previous || 'Previous'}\n \n { runsPage++; loadRunHistory(); }}\n disabled={runsPage * runsPageSize >= runsTotal}\n class=\"px-3 py-1.5 text-sm border border-gray-200 rounded-lg disabled:opacity-50 hover:bg-gray-50\"\n >\n {$t.validation?.next || 'Next'}\n \n
\n
\n {/if}\n {/if}\n
\n {/if}\n {/if}\n
\n\n" }, { "contract_id": "ValidationHistory", @@ -73214,7 +74553,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region GitServiceContractTests [C:3] [TYPE Module] [SEMANTICS git, service, test, contract, api]\n// @BRIEF API client tests ensuring correct endpoints are called per contract for Git service operations.\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n\n// #region gitServiceContractTests:Module [TYPE Function]\n// @SEMANTICS: git-service, api-client, contract-tests\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n// @PURPOSE: API client tests ensuring correct endpoints are called per contract\n// @LAYER Tests\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { gitService } from '../gitService';\nimport { requestApi } from '../../lib/api';\n\nvi.mock('../../lib/api', () => ({\n requestApi: vi.fn()\n}));\n\ndescribe('gitService', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe('Config Management', () => {\n // @PRE User authenticated\n // @POST Returns list of Git configs\n it('getConfigs calls /git/config', async () => {\n requestApi.mockResolvedValue([]);\n const result = await gitService.getConfigs();\n expect(requestApi).toHaveBeenCalledWith('/git/config');\n expect(result).toEqual([]);\n });\n\n // @PRE Config object must be valid\n // @POST New config created and returned\n it('createConfig calls POST /git/config', async () => {\n const config = { name: 'Test', provider: 'GITHUB' };\n requestApi.mockResolvedValue({ id: '1', ...config });\n const result = await gitService.createConfig(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'POST', config);\n expect(result.id).toBe('1');\n });\n\n // @PRE configId must exist\n // @POST Config deleted\n it('deleteConfig calls DELETE /git/config/:id', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteConfig('config-123');\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'DELETE');\n });\n\n // @PRE configId must exist\n // @POST Config updated\n it('updateConfig calls PUT /git/config/:id', async () => {\n const config = { name: 'Updated' };\n requestApi.mockResolvedValue({ id: 'config-123', ...config });\n await gitService.updateConfig('config-123', config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'PUT', config);\n });\n\n // @PRE Config must contain valid URL and PAT\n // @POST Returns connection status\n it('testConnection calls POST /git/config/test', async () => {\n const config = { url: 'https://github.com', pat: 'secret' };\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.testConnection(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/test', 'POST', config);\n });\n });\n\n describe('Remote Repository Management', () => {\n // @PRE configId references GITEA\n // @POST Returns repo metadata\n it('listGiteaRepositories calls GET /git/config/:id/gitea/repos', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.listGiteaRepositories('conf-1');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos');\n });\n\n // @PRE configId references GITEA\n // @POST Repo created on Gitea\n it('createGiteaRepository calls POST /git/config/:id/gitea/repos', async () => {\n const payload = { name: 'new-repo' };\n requestApi.mockResolvedValue({ name: 'new-repo' });\n await gitService.createGiteaRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'POST', payload);\n });\n\n // @PRE configId references supported provider\n // @POST Remote repo created\n it('createRemoteRepository calls POST /git/config/:id/repositories', async () => {\n const payload = { name: 'new-remote' };\n requestApi.mockResolvedValue({ name: 'new-remote' });\n await gitService.createRemoteRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/repositories', 'POST', payload);\n });\n\n // @PRE configId references GITEA\n // @POST Repo deleted on Gitea\n it('deleteGiteaRepository calls DELETE with encoded owner and repo', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteGiteaRepository('conf-1', 'my-org', 'my-repo');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos/my-org/my-repo', 'DELETE');\n });\n });\n\n describe('Dashboard Repository Operations', () => {\n // @PRE Dashboard exists and config_id valid\n // @POST Repo initialized\n it('initRepository calls POST with remote_url and config_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.initRepository(123, 'conf-1', 'http://remote.git');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/init', 'POST', {\n config_id: 'conf-1', remote_url: 'http://remote.git'\n });\n });\n\n // @PRE Repo initialized\n // @POST Returns binding\n it('getRepositoryBinding calls GET', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getRepositoryBinding('dash-slug');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/dash-slug');\n });\n\n // @PRE Repo initialized\n // @POST Returns branches\n it('getBranches calls GET /branches', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getBranches(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches');\n });\n\n // @PRE Source branch exists\n // @POST New branch created\n it('createBranch calls POST /branches with name and from_branch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.createBranch(123, 'feature', 'main');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'POST', {\n name: 'feature', from_branch: 'main'\n });\n });\n\n // @PRE Target branch exists\n // @POST Repo checked out\n it('checkoutBranch calls POST /checkout', async () => {\n requestApi.mockResolvedValue({});\n await gitService.checkoutBranch(123, 'feature');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/checkout', 'POST', { name: 'feature' });\n });\n\n // @PRE Message not empty\n // @POST Changes committed\n it('commit calls POST /commit with message and files', async () => {\n requestApi.mockResolvedValue({});\n await gitService.commit(123, 'Initial commit', ['file.txt']);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/commit', 'POST', {\n message: 'Initial commit', files: ['file.txt']\n });\n });\n\n // @PRE Remote configured\n // @POST Remote updated\n it('push calls POST /push', async () => {\n requestApi.mockResolvedValue({});\n await gitService.push(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/push', 'POST');\n });\n\n // @PRE Dashboard ref valid\n // @POST Repo deleted\n it('deleteRepository calls DELETE', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deleteRepository(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123', 'DELETE');\n });\n\n // @PRE Remote configured\n // @POST Local repo updated\n it('pull calls POST /pull', async () => {\n requestApi.mockResolvedValue({});\n await gitService.pull(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/pull', 'POST');\n });\n });\n\n describe('Other Operations', () => {\n // @POST Returns list of environments\n it('getEnvironments calls GET /environments', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getEnvironments();\n expect(requestApi).toHaveBeenCalledWith('/git/environments');\n });\n\n // @PRE Environment active\n // @POST Dashboard deployed\n it('deploy calls POST /deploy with environment_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deploy(123, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/deploy', 'POST', { environment_id: 'env-1' });\n });\n\n // @POST Returns commit history\n it('getHistory calls GET /history with limit', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getHistory(123, 10);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/history?limit=10');\n });\n\n // @POST Syncs local and remote\n it('sync calls POST /sync with optional params', async () => {\n requestApi.mockResolvedValue({});\n await gitService.sync(123, 'env-source', 'env-target');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/sync?source_env_id=env-source&env_id=env-target', 'POST');\n });\n\n // @PRE Dashboard id valid\n // @POST Returns status\n it('getStatus calls GET /status', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatus(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/status');\n });\n\n // @PRE Dashboard ids array\n // @POST Returns batch statuses\n it('getStatusesBatch calls POST /status/batch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatusesBatch([1, 2, 3]);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/status/batch', 'POST', { dashboard_ids: [1, 2, 3] });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns diff\n it('getDiff calls GET /diff with params', async () => {\n requestApi.mockResolvedValue('');\n await gitService.getDiff(123, 'file.txt', true, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/diff?file_path=file.txt&staged=true&env_id=env-1');\n });\n\n // @PRE Repo initialized\n // @POST Returns promotion metadata\n it('promote calls POST /promote with payload', async () => {\n const payload = { from_branch: 'dev', to_branch: 'main', mode: 'mr' };\n requestApi.mockResolvedValue({});\n await gitService.promote(123, payload);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/promote', 'POST', payload);\n });\n });\n});\n// #endregion gitServiceContractTests:Module\n// #endregion GitServiceContractTests\n" + "body": "// #region GitServiceContractTests [C:3] [TYPE Module] [SEMANTICS git, service, test, contract, api]\n// @BRIEF API client tests ensuring correct endpoints are called per contract for Git service operations.\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n\n// #region gitServiceContractTests:Module [TYPE Function]\n// @SEMANTICS: git-service, api-client, contract-tests\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n// @PURPOSE: API client tests ensuring correct endpoints are called per contract\n// @LAYER Tests\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { gitService } from '../gitService';\nimport { requestApi } from '../../lib/api';\n\nvi.mock('../../lib/api', () => ({\n requestApi: vi.fn()\n}));\n\ndescribe('gitService', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe('Config Management', () => {\n // @PRE User authenticated\n // @POST Returns list of Git configs\n it('getConfigs calls /git/config', async () => {\n requestApi.mockResolvedValue([]);\n const result = await gitService.getConfigs();\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'GET', null, { suppressToast: true });\n expect(result).toEqual([]);\n });\n\n // @PRE Config object must be valid\n // @POST New config created and returned\n it('createConfig calls POST /git/config', async () => {\n const config = { name: 'Test', provider: 'GITHUB' };\n requestApi.mockResolvedValue({ id: '1', ...config });\n const result = await gitService.createConfig(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'POST', config, { suppressToast: true });\n expect(result.id).toBe('1');\n });\n\n // @PRE configId must exist\n // @POST Config deleted\n it('deleteConfig calls DELETE /git/config/:id', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteConfig('config-123');\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'DELETE', null, { suppressToast: true });\n });\n\n // @PRE configId must exist\n // @POST Config updated\n it('updateConfig calls PUT /git/config/:id', async () => {\n const config = { name: 'Updated' };\n requestApi.mockResolvedValue({ id: 'config-123', ...config });\n await gitService.updateConfig('config-123', config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'PUT', config, { suppressToast: true });\n });\n\n // @PRE Config must contain valid URL and PAT\n // @POST Returns connection status\n it('testConnection calls POST /git/config/test', async () => {\n const config = { url: 'https://github.com', pat: 'secret' };\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.testConnection(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/test', 'POST', config, { suppressToast: true });\n });\n });\n\n describe('Remote Repository Management', () => {\n // @PRE configId references GITEA\n // @POST Returns repo metadata\n it('listGiteaRepositories calls GET /git/config/:id/gitea/repos', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.listGiteaRepositories('conf-1');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'GET', null, { suppressToast: true });\n });\n\n // @PRE configId references GITEA\n // @POST Repo created on Gitea\n it('createGiteaRepository calls POST /git/config/:id/gitea/repos', async () => {\n const payload = { name: 'new-repo' };\n requestApi.mockResolvedValue({ name: 'new-repo' });\n await gitService.createGiteaRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'POST', payload, { suppressToast: true });\n });\n\n // @PRE configId references supported provider\n // @POST Remote repo created\n it('createRemoteRepository calls POST /git/config/:id/repositories', async () => {\n const payload = { name: 'new-remote' };\n requestApi.mockResolvedValue({ name: 'new-remote' });\n await gitService.createRemoteRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/repositories', 'POST', payload, { suppressToast: true });\n });\n\n // @PRE configId references GITEA\n // @POST Repo deleted on Gitea\n it('deleteGiteaRepository calls DELETE with encoded owner and repo', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteGiteaRepository('conf-1', 'my-org', 'my-repo');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos/my-org/my-repo', 'DELETE', null, { suppressToast: true });\n });\n });\n\n describe('Dashboard Repository Operations', () => {\n // @PRE Dashboard exists and config_id valid\n // @POST Repo initialized\n it('initRepository calls POST with remote_url and config_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.initRepository(123, 'conf-1', 'http://remote.git');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/init', 'POST', {\n config_id: 'conf-1', remote_url: 'http://remote.git'\n }, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns binding\n it('getRepositoryBinding calls GET', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getRepositoryBinding('dash-slug');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/dash-slug', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns branches\n it('getBranches calls GET /branches', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getBranches(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Source branch exists\n // @POST New branch created\n it('createBranch calls POST /branches with name and from_branch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.createBranch(123, 'feature', 'main');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'POST', {\n name: 'feature', from_branch: 'main'\n }, { suppressToast: true });\n });\n\n // @PRE Target branch exists\n // @POST Repo checked out\n it('checkoutBranch calls POST /checkout', async () => {\n requestApi.mockResolvedValue({});\n await gitService.checkoutBranch(123, 'feature');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/checkout', 'POST', { name: 'feature' }, { suppressToast: true });\n });\n\n // @PRE Message not empty\n // @POST Changes committed\n it('commit calls POST /commit with message and files', async () => {\n requestApi.mockResolvedValue({});\n await gitService.commit(123, 'Initial commit', ['file.txt']);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/commit', 'POST', {\n message: 'Initial commit', files: ['file.txt']\n }, { suppressToast: true });\n });\n\n // @PRE Remote configured\n // @POST Remote updated\n it('push calls POST /push', async () => {\n requestApi.mockResolvedValue({});\n await gitService.push(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/push', 'POST', null, { suppressToast: true });\n });\n\n // @PRE Dashboard ref valid\n // @POST Repo deleted\n it('deleteRepository calls DELETE', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deleteRepository(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123', 'DELETE', null, { suppressToast: true });\n });\n\n // @PRE Remote configured\n // @POST Local repo updated\n it('pull calls POST /pull', async () => {\n requestApi.mockResolvedValue({});\n await gitService.pull(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/pull', 'POST', null, { suppressToast: true });\n });\n });\n\n describe('Other Operations', () => {\n // @POST Returns list of environments\n it('getEnvironments calls GET /environments', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getEnvironments();\n expect(requestApi).toHaveBeenCalledWith('/git/environments', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Environment active\n // @POST Dashboard deployed\n it('deploy calls POST /deploy with environment_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deploy(123, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/deploy', 'POST', { environment_id: 'env-1' }, { suppressToast: true });\n });\n\n // @POST Returns commit history\n it('getHistory calls GET /history with limit', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getHistory(123, 10);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/history?limit=10', 'GET', null, { suppressToast: true });\n });\n\n // @POST Syncs local and remote\n it('sync calls POST /sync with optional params', async () => {\n requestApi.mockResolvedValue({});\n await gitService.sync(123, 'env-source', 'env-target');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/sync?source_env_id=env-source&env_id=env-target', 'POST', null, { suppressToast: true });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns status\n it('getStatus calls GET /status', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatus(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/status', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Dashboard ids array\n // @POST Returns batch statuses\n it('getStatusesBatch calls POST /status/batch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatusesBatch([1, 2, 3]);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/status/batch', 'POST', { dashboard_ids: [1, 2, 3] }, { suppressToast: true });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns diff\n it('getDiff calls GET /diff with params', async () => {\n requestApi.mockResolvedValue('');\n await gitService.getDiff(123, 'file.txt', true, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/diff?file_path=file.txt&staged=true&env_id=env-1', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns promotion metadata\n it('promote calls POST /promote with payload', async () => {\n const payload = { from_branch: 'dev', to_branch: 'main', mode: 'mr' };\n requestApi.mockResolvedValue({});\n await gitService.promote(123, payload);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/promote', 'POST', payload, { suppressToast: true });\n });\n });\n});\n// #endregion gitServiceContractTests:Module\n// #endregion GitServiceContractTests\n" }, { "contract_id": "gitServiceContractTests:Module", @@ -73246,7 +74585,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region gitServiceContractTests:Module [TYPE Function]\n// @SEMANTICS: git-service, api-client, contract-tests\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n// @PURPOSE: API client tests ensuring correct endpoints are called per contract\n// @LAYER Tests\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { gitService } from '../gitService';\nimport { requestApi } from '../../lib/api';\n\nvi.mock('../../lib/api', () => ({\n requestApi: vi.fn()\n}));\n\ndescribe('gitService', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe('Config Management', () => {\n // @PRE User authenticated\n // @POST Returns list of Git configs\n it('getConfigs calls /git/config', async () => {\n requestApi.mockResolvedValue([]);\n const result = await gitService.getConfigs();\n expect(requestApi).toHaveBeenCalledWith('/git/config');\n expect(result).toEqual([]);\n });\n\n // @PRE Config object must be valid\n // @POST New config created and returned\n it('createConfig calls POST /git/config', async () => {\n const config = { name: 'Test', provider: 'GITHUB' };\n requestApi.mockResolvedValue({ id: '1', ...config });\n const result = await gitService.createConfig(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'POST', config);\n expect(result.id).toBe('1');\n });\n\n // @PRE configId must exist\n // @POST Config deleted\n it('deleteConfig calls DELETE /git/config/:id', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteConfig('config-123');\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'DELETE');\n });\n\n // @PRE configId must exist\n // @POST Config updated\n it('updateConfig calls PUT /git/config/:id', async () => {\n const config = { name: 'Updated' };\n requestApi.mockResolvedValue({ id: 'config-123', ...config });\n await gitService.updateConfig('config-123', config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'PUT', config);\n });\n\n // @PRE Config must contain valid URL and PAT\n // @POST Returns connection status\n it('testConnection calls POST /git/config/test', async () => {\n const config = { url: 'https://github.com', pat: 'secret' };\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.testConnection(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/test', 'POST', config);\n });\n });\n\n describe('Remote Repository Management', () => {\n // @PRE configId references GITEA\n // @POST Returns repo metadata\n it('listGiteaRepositories calls GET /git/config/:id/gitea/repos', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.listGiteaRepositories('conf-1');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos');\n });\n\n // @PRE configId references GITEA\n // @POST Repo created on Gitea\n it('createGiteaRepository calls POST /git/config/:id/gitea/repos', async () => {\n const payload = { name: 'new-repo' };\n requestApi.mockResolvedValue({ name: 'new-repo' });\n await gitService.createGiteaRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'POST', payload);\n });\n\n // @PRE configId references supported provider\n // @POST Remote repo created\n it('createRemoteRepository calls POST /git/config/:id/repositories', async () => {\n const payload = { name: 'new-remote' };\n requestApi.mockResolvedValue({ name: 'new-remote' });\n await gitService.createRemoteRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/repositories', 'POST', payload);\n });\n\n // @PRE configId references GITEA\n // @POST Repo deleted on Gitea\n it('deleteGiteaRepository calls DELETE with encoded owner and repo', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteGiteaRepository('conf-1', 'my-org', 'my-repo');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos/my-org/my-repo', 'DELETE');\n });\n });\n\n describe('Dashboard Repository Operations', () => {\n // @PRE Dashboard exists and config_id valid\n // @POST Repo initialized\n it('initRepository calls POST with remote_url and config_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.initRepository(123, 'conf-1', 'http://remote.git');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/init', 'POST', {\n config_id: 'conf-1', remote_url: 'http://remote.git'\n });\n });\n\n // @PRE Repo initialized\n // @POST Returns binding\n it('getRepositoryBinding calls GET', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getRepositoryBinding('dash-slug');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/dash-slug');\n });\n\n // @PRE Repo initialized\n // @POST Returns branches\n it('getBranches calls GET /branches', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getBranches(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches');\n });\n\n // @PRE Source branch exists\n // @POST New branch created\n it('createBranch calls POST /branches with name and from_branch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.createBranch(123, 'feature', 'main');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'POST', {\n name: 'feature', from_branch: 'main'\n });\n });\n\n // @PRE Target branch exists\n // @POST Repo checked out\n it('checkoutBranch calls POST /checkout', async () => {\n requestApi.mockResolvedValue({});\n await gitService.checkoutBranch(123, 'feature');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/checkout', 'POST', { name: 'feature' });\n });\n\n // @PRE Message not empty\n // @POST Changes committed\n it('commit calls POST /commit with message and files', async () => {\n requestApi.mockResolvedValue({});\n await gitService.commit(123, 'Initial commit', ['file.txt']);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/commit', 'POST', {\n message: 'Initial commit', files: ['file.txt']\n });\n });\n\n // @PRE Remote configured\n // @POST Remote updated\n it('push calls POST /push', async () => {\n requestApi.mockResolvedValue({});\n await gitService.push(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/push', 'POST');\n });\n\n // @PRE Dashboard ref valid\n // @POST Repo deleted\n it('deleteRepository calls DELETE', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deleteRepository(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123', 'DELETE');\n });\n\n // @PRE Remote configured\n // @POST Local repo updated\n it('pull calls POST /pull', async () => {\n requestApi.mockResolvedValue({});\n await gitService.pull(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/pull', 'POST');\n });\n });\n\n describe('Other Operations', () => {\n // @POST Returns list of environments\n it('getEnvironments calls GET /environments', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getEnvironments();\n expect(requestApi).toHaveBeenCalledWith('/git/environments');\n });\n\n // @PRE Environment active\n // @POST Dashboard deployed\n it('deploy calls POST /deploy with environment_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deploy(123, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/deploy', 'POST', { environment_id: 'env-1' });\n });\n\n // @POST Returns commit history\n it('getHistory calls GET /history with limit', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getHistory(123, 10);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/history?limit=10');\n });\n\n // @POST Syncs local and remote\n it('sync calls POST /sync with optional params', async () => {\n requestApi.mockResolvedValue({});\n await gitService.sync(123, 'env-source', 'env-target');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/sync?source_env_id=env-source&env_id=env-target', 'POST');\n });\n\n // @PRE Dashboard id valid\n // @POST Returns status\n it('getStatus calls GET /status', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatus(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/status');\n });\n\n // @PRE Dashboard ids array\n // @POST Returns batch statuses\n it('getStatusesBatch calls POST /status/batch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatusesBatch([1, 2, 3]);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/status/batch', 'POST', { dashboard_ids: [1, 2, 3] });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns diff\n it('getDiff calls GET /diff with params', async () => {\n requestApi.mockResolvedValue('');\n await gitService.getDiff(123, 'file.txt', true, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/diff?file_path=file.txt&staged=true&env_id=env-1');\n });\n\n // @PRE Repo initialized\n // @POST Returns promotion metadata\n it('promote calls POST /promote with payload', async () => {\n const payload = { from_branch: 'dev', to_branch: 'main', mode: 'mr' };\n requestApi.mockResolvedValue({});\n await gitService.promote(123, payload);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/promote', 'POST', payload);\n });\n });\n});\n// #endregion gitServiceContractTests:Module\n" + "body": "// #region gitServiceContractTests:Module [TYPE Function]\n// @SEMANTICS: git-service, api-client, contract-tests\n// @RELATION DEPENDS_ON -> [EXT:frontend:GitServiceClient]\n// @PURPOSE: API client tests ensuring correct endpoints are called per contract\n// @LAYER Tests\n\nimport { describe, it, expect, vi, beforeEach } from 'vitest';\nimport { gitService } from '../gitService';\nimport { requestApi } from '../../lib/api';\n\nvi.mock('../../lib/api', () => ({\n requestApi: vi.fn()\n}));\n\ndescribe('gitService', () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe('Config Management', () => {\n // @PRE User authenticated\n // @POST Returns list of Git configs\n it('getConfigs calls /git/config', async () => {\n requestApi.mockResolvedValue([]);\n const result = await gitService.getConfigs();\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'GET', null, { suppressToast: true });\n expect(result).toEqual([]);\n });\n\n // @PRE Config object must be valid\n // @POST New config created and returned\n it('createConfig calls POST /git/config', async () => {\n const config = { name: 'Test', provider: 'GITHUB' };\n requestApi.mockResolvedValue({ id: '1', ...config });\n const result = await gitService.createConfig(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config', 'POST', config, { suppressToast: true });\n expect(result.id).toBe('1');\n });\n\n // @PRE configId must exist\n // @POST Config deleted\n it('deleteConfig calls DELETE /git/config/:id', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteConfig('config-123');\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'DELETE', null, { suppressToast: true });\n });\n\n // @PRE configId must exist\n // @POST Config updated\n it('updateConfig calls PUT /git/config/:id', async () => {\n const config = { name: 'Updated' };\n requestApi.mockResolvedValue({ id: 'config-123', ...config });\n await gitService.updateConfig('config-123', config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/config-123', 'PUT', config, { suppressToast: true });\n });\n\n // @PRE Config must contain valid URL and PAT\n // @POST Returns connection status\n it('testConnection calls POST /git/config/test', async () => {\n const config = { url: 'https://github.com', pat: 'secret' };\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.testConnection(config);\n expect(requestApi).toHaveBeenCalledWith('/git/config/test', 'POST', config, { suppressToast: true });\n });\n });\n\n describe('Remote Repository Management', () => {\n // @PRE configId references GITEA\n // @POST Returns repo metadata\n it('listGiteaRepositories calls GET /git/config/:id/gitea/repos', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.listGiteaRepositories('conf-1');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'GET', null, { suppressToast: true });\n });\n\n // @PRE configId references GITEA\n // @POST Repo created on Gitea\n it('createGiteaRepository calls POST /git/config/:id/gitea/repos', async () => {\n const payload = { name: 'new-repo' };\n requestApi.mockResolvedValue({ name: 'new-repo' });\n await gitService.createGiteaRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos', 'POST', payload, { suppressToast: true });\n });\n\n // @PRE configId references supported provider\n // @POST Remote repo created\n it('createRemoteRepository calls POST /git/config/:id/repositories', async () => {\n const payload = { name: 'new-remote' };\n requestApi.mockResolvedValue({ name: 'new-remote' });\n await gitService.createRemoteRepository('conf-1', payload);\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/repositories', 'POST', payload, { suppressToast: true });\n });\n\n // @PRE configId references GITEA\n // @POST Repo deleted on Gitea\n it('deleteGiteaRepository calls DELETE with encoded owner and repo', async () => {\n requestApi.mockResolvedValue({ status: 'success' });\n await gitService.deleteGiteaRepository('conf-1', 'my-org', 'my-repo');\n expect(requestApi).toHaveBeenCalledWith('/git/config/conf-1/gitea/repos/my-org/my-repo', 'DELETE', null, { suppressToast: true });\n });\n });\n\n describe('Dashboard Repository Operations', () => {\n // @PRE Dashboard exists and config_id valid\n // @POST Repo initialized\n it('initRepository calls POST with remote_url and config_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.initRepository(123, 'conf-1', 'http://remote.git');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/init', 'POST', {\n config_id: 'conf-1', remote_url: 'http://remote.git'\n }, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns binding\n it('getRepositoryBinding calls GET', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getRepositoryBinding('dash-slug');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/dash-slug', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns branches\n it('getBranches calls GET /branches', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getBranches(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Source branch exists\n // @POST New branch created\n it('createBranch calls POST /branches with name and from_branch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.createBranch(123, 'feature', 'main');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/branches', 'POST', {\n name: 'feature', from_branch: 'main'\n }, { suppressToast: true });\n });\n\n // @PRE Target branch exists\n // @POST Repo checked out\n it('checkoutBranch calls POST /checkout', async () => {\n requestApi.mockResolvedValue({});\n await gitService.checkoutBranch(123, 'feature');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/checkout', 'POST', { name: 'feature' }, { suppressToast: true });\n });\n\n // @PRE Message not empty\n // @POST Changes committed\n it('commit calls POST /commit with message and files', async () => {\n requestApi.mockResolvedValue({});\n await gitService.commit(123, 'Initial commit', ['file.txt']);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/commit', 'POST', {\n message: 'Initial commit', files: ['file.txt']\n }, { suppressToast: true });\n });\n\n // @PRE Remote configured\n // @POST Remote updated\n it('push calls POST /push', async () => {\n requestApi.mockResolvedValue({});\n await gitService.push(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/push', 'POST', null, { suppressToast: true });\n });\n\n // @PRE Dashboard ref valid\n // @POST Repo deleted\n it('deleteRepository calls DELETE', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deleteRepository(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123', 'DELETE', null, { suppressToast: true });\n });\n\n // @PRE Remote configured\n // @POST Local repo updated\n it('pull calls POST /pull', async () => {\n requestApi.mockResolvedValue({});\n await gitService.pull(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/pull', 'POST', null, { suppressToast: true });\n });\n });\n\n describe('Other Operations', () => {\n // @POST Returns list of environments\n it('getEnvironments calls GET /environments', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getEnvironments();\n expect(requestApi).toHaveBeenCalledWith('/git/environments', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Environment active\n // @POST Dashboard deployed\n it('deploy calls POST /deploy with environment_id', async () => {\n requestApi.mockResolvedValue({});\n await gitService.deploy(123, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/deploy', 'POST', { environment_id: 'env-1' }, { suppressToast: true });\n });\n\n // @POST Returns commit history\n it('getHistory calls GET /history with limit', async () => {\n requestApi.mockResolvedValue([]);\n await gitService.getHistory(123, 10);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/history?limit=10', 'GET', null, { suppressToast: true });\n });\n\n // @POST Syncs local and remote\n it('sync calls POST /sync with optional params', async () => {\n requestApi.mockResolvedValue({});\n await gitService.sync(123, 'env-source', 'env-target');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/sync?source_env_id=env-source&env_id=env-target', 'POST', null, { suppressToast: true });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns status\n it('getStatus calls GET /status', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatus(123);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/status', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Dashboard ids array\n // @POST Returns batch statuses\n it('getStatusesBatch calls POST /status/batch', async () => {\n requestApi.mockResolvedValue({});\n await gitService.getStatusesBatch([1, 2, 3]);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/status/batch', 'POST', { dashboard_ids: [1, 2, 3] }, { suppressToast: true });\n });\n\n // @PRE Dashboard id valid\n // @POST Returns diff\n it('getDiff calls GET /diff with params', async () => {\n requestApi.mockResolvedValue('');\n await gitService.getDiff(123, 'file.txt', true, 'env-1');\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/diff?file_path=file.txt&staged=true&env_id=env-1', 'GET', null, { suppressToast: true });\n });\n\n // @PRE Repo initialized\n // @POST Returns promotion metadata\n it('promote calls POST /promote with payload', async () => {\n const payload = { from_branch: 'dev', to_branch: 'main', mode: 'mr' };\n requestApi.mockResolvedValue({});\n await gitService.promote(123, payload);\n expect(requestApi).toHaveBeenCalledWith('/git/repositories/123/promote', 'POST', payload, { suppressToast: true });\n });\n });\n});\n// #endregion gitServiceContractTests:Module\n" }, { "contract_id": "AdminService", @@ -74057,7 +75396,7 @@ "contract_type": "Service", "file_path": "frontend/src/services/gitService.js", "start_line": 1, - "end_line": 368, + "end_line": 379, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -74076,7 +75415,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region GitServiceClient [C:3] [TYPE Service] [SEMANTICS git, service, api, repository, branch]\n// @BRIEF API client for Git operations — repository management, branches, commits, merges, deployments.\n// @LAYER Service\n// @RELATION DEPENDS_ON -> [ApiModule]\n\nimport { requestApi } from '../lib/api';\n\nconst API_BASE = '/git';\n\n// #region buildDashboardRepoEndpoint [C:2] [TYPE Function]\n// @BRIEF Build /git/repositories/{ref}{suffix} endpoint with optional env_id query param.\n// @PRE dashboardRef is non-empty string.\n// @POST Returns absolute API endpoint path with env_id appended when envId is provided.\n// @DATA_CONTRACT Input(dashboardRef, suffix, envId?) -> Output(string)\nfunction buildDashboardRepoEndpoint(dashboardRef, suffix, envId = null) {\n const encodedRef = encodeURIComponent(String(dashboardRef));\n const endpoint = `${API_BASE}/repositories/${encodedRef}${suffix}`;\n if (!envId) return endpoint;\n const sep = endpoint.includes('?') ? '&' : '?';\n return `${endpoint}${sep}env_id=${encodeURIComponent(String(envId))}`;\n}\n// #endregion buildDashboardRepoEndpoint\n\n// #region gitService [C:3] [TYPE Object]\n// @BRIEF Exported Git API client with methods for all repository and config operations.\n// @RELATION CALLS -> [ApiModule.requestApi]\nexport const gitService = {\n\n // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return requestApi(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n\n // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return requestApi(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n\n // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n\n // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n\n // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return requestApi(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n\n // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n\n // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n\n // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n\n // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n\n // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n\n // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n\n // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n\n // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n\n // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n\n // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n\n // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n\n // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n\n // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n\n // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n\n // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n\n // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n\n // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n\n // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n\n // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return requestApi(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n\n // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n\n // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n\n // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return requestApi(endpoint, 'POST');\n },\n // #endregion sync\n\n // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n\n // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return requestApi(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n\n // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return requestApi(endpoint);\n },\n // #endregion getDiff\n\n // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n};\n// #endregion gitService\n// #endregion GitServiceClient\n" + "body": "// #region GitServiceClient [C:3] [TYPE Service] [SEMANTICS git, service, api, repository, branch]\n// @BRIEF API client for Git operations — repository management, branches, commits, merges, deployments.\n// @LAYER Service\n// @RELATION DEPENDS_ON -> [ApiModule]\n\nimport { requestApi } from '../lib/api';\n\nconst API_BASE = '/git';\n\n// #region buildDashboardRepoEndpoint [C:2] [TYPE Function]\n// @BRIEF Build /git/repositories/{ref}{suffix} endpoint with optional env_id query param.\n// @PRE dashboardRef is non-empty string.\n// @POST Returns absolute API endpoint path with env_id appended when envId is provided.\n// @DATA_CONTRACT Input(dashboardRef, suffix, envId?) -> Output(string)\nfunction buildDashboardRepoEndpoint(dashboardRef, suffix, envId = null) {\n const encodedRef = encodeURIComponent(String(dashboardRef));\n const endpoint = `${API_BASE}/repositories/${encodedRef}${suffix}`;\n if (!envId) return endpoint;\n const sep = endpoint.includes('?') ? '&' : '?';\n return `${endpoint}${sep}env_id=${encodeURIComponent(String(envId))}`;\n}\n// #endregion buildDashboardRepoEndpoint\n\n// #region gitRequest [C:2] [TYPE Function]\n// @BRIEF Wrapper around requestApi that auto-suppresses API-level toasts —\n// Git error display is handled by useGitManager.js handlers (persistent toasts + modal banner).\n// @PRE Works like requestApi(endpoint, method, body, extraOptions).\n// @POST Api-level toast suppressed; error propagates to caller for handler-level display.\nasync function gitRequest(endpoint, method = 'GET', body = null, extraOptions = {}) {\n const requestOptions = { ...extraOptions, suppressToast: true };\n return requestApi(endpoint, method, body, requestOptions);\n}\n// #endregion gitRequest\n\n// #region gitService [C:3] [TYPE Object]\n// @BRIEF Exported Git API client with methods for all repository and config operations.\n// @RELATION CALLS -> [ApiModule.requestApi]\nexport const gitService = {\n\n // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return gitRequest(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n\n // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return gitRequest(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n\n // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n\n // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n\n // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return gitRequest(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n\n // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n\n // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n\n // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n\n // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n\n // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n\n // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n\n // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n\n // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n\n // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n\n // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n\n // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n\n // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n\n // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n\n // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n\n // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n\n // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n\n // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n\n // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n\n // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return gitRequest(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n\n // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n\n // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n\n // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return gitRequest(endpoint, 'POST');\n },\n // #endregion sync\n\n // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n\n // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return gitRequest(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n\n // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return gitRequest(endpoint);\n },\n // #endregion getDiff\n\n // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n};\n// #endregion gitService\n// #endregion GitServiceClient\n" }, { "contract_id": "buildDashboardRepoEndpoint", @@ -74099,12 +75438,30 @@ "tier_source": "AutoCalculated", "body": "// #region buildDashboardRepoEndpoint [C:2] [TYPE Function]\n// @BRIEF Build /git/repositories/{ref}{suffix} endpoint with optional env_id query param.\n// @PRE dashboardRef is non-empty string.\n// @POST Returns absolute API endpoint path with env_id appended when envId is provided.\n// @DATA_CONTRACT Input(dashboardRef, suffix, envId?) -> Output(string)\nfunction buildDashboardRepoEndpoint(dashboardRef, suffix, envId = null) {\n const encodedRef = encodeURIComponent(String(dashboardRef));\n const endpoint = `${API_BASE}/repositories/${encodedRef}${suffix}`;\n if (!envId) return endpoint;\n const sep = endpoint.includes('?') ? '&' : '?';\n return `${endpoint}${sep}env_id=${encodeURIComponent(String(envId))}`;\n}\n// #endregion buildDashboardRepoEndpoint\n" }, + { + "contract_id": "gitRequest", + "contract_type": "Function", + "file_path": "frontend/src/services/gitService.js", + "start_line": 24, + "end_line": 33, + "tier": "TIER_1", + "complexity": 2, + "metadata": { + "BRIEF": "Wrapper around requestApi that auto-suppresses API-level toasts —", + "COMPLEXITY": 2 + }, + "relations": [], + "schema_warnings": [], + "anchor_syntax": "region", + "tier_source": "AutoCalculated", + "body": "// #region gitRequest [C:2] [TYPE Function]\n// @BRIEF Wrapper around requestApi that auto-suppresses API-level toasts —\n// Git error display is handled by useGitManager.js handlers (persistent toasts + modal banner).\n// @PRE Works like requestApi(endpoint, method, body, extraOptions).\n// @POST Api-level toast suppressed; error propagates to caller for handler-level display.\nasync function gitRequest(endpoint, method = 'GET', body = null, extraOptions = {}) {\n const requestOptions = { ...extraOptions, suppressToast: true };\n return requestApi(endpoint, method, body, requestOptions);\n}\n// #endregion gitRequest\n" + }, { "contract_id": "gitService", "contract_type": "Object", "file_path": "frontend/src/services/gitService.js", - "start_line": 24, - "end_line": 367, + "start_line": 35, + "end_line": 378, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -74122,14 +75479,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": "// #region gitService [C:3] [TYPE Object]\n// @BRIEF Exported Git API client with methods for all repository and config operations.\n// @RELATION CALLS -> [ApiModule.requestApi]\nexport const gitService = {\n\n // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return requestApi(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n\n // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return requestApi(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n\n // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n\n // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n\n // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return requestApi(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n\n // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n\n // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n\n // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n\n // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n\n // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n\n // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n\n // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n\n // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n\n // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n\n // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n\n // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n\n // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n\n // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n\n // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n\n // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n\n // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n\n // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n\n // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n\n // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return requestApi(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n\n // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n\n // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n\n // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return requestApi(endpoint, 'POST');\n },\n // #endregion sync\n\n // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n\n // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return requestApi(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n\n // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return requestApi(endpoint);\n },\n // #endregion getDiff\n\n // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n};\n// #endregion gitService\n" + "body": "// #region gitService [C:3] [TYPE Object]\n// @BRIEF Exported Git API client with methods for all repository and config operations.\n// @RELATION CALLS -> [ApiModule.requestApi]\nexport const gitService = {\n\n // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return gitRequest(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n\n // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return gitRequest(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n\n // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n\n // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n\n // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return gitRequest(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n\n // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n\n // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n\n // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n\n // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n\n // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n\n // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n\n // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n\n // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n\n // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n\n // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n\n // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n\n // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n\n // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n\n // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n\n // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n\n // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n\n // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n\n // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n\n // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return gitRequest(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n\n // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n\n // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n\n // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return gitRequest(endpoint, 'POST');\n },\n // #endregion sync\n\n // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n\n // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return gitRequest(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n\n // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return gitRequest(endpoint);\n },\n // #endregion getDiff\n\n // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n};\n// #endregion gitService\n" }, { "contract_id": "getConfigs", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 29, - "end_line": 38, + "start_line": 40, + "end_line": 49, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74143,14 +75500,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return requestApi(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n" + "body": " // #region getConfigs [C:2] [TYPE Function]\n // @BRIEF Fetch all Git server configurations.\n // @PRE User is authenticated (handled by ApiModule auth headers).\n // @POST Returns list of GitServerConfig objects.\n // @DATA_CONTRACT None -> Array\n async getConfigs() {\n console.log('[getConfigs][Action] Fetching Git configs');\n return gitRequest(`${API_BASE}/config`);\n },\n // #endregion getConfigs\n" }, { "contract_id": "createConfig", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 40, - "end_line": 49, + "start_line": 51, + "end_line": 60, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74164,14 +75521,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return requestApi(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n" + "body": " // #region createConfig [C:2] [TYPE Function]\n // @BRIEF Create a new Git server configuration.\n // @PRE config object is valid with required fields.\n // @POST New config is persisted and returned.\n // @DATA_CONTRACT Input(GitServerConfig) -> Output(GitServerConfig)\n async createConfig(config) {\n console.log('[createConfig][Action] Creating Git config');\n return gitRequest(`${API_BASE}/config`, 'POST', config);\n },\n // #endregion createConfig\n" }, { "contract_id": "deleteConfig", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 51, - "end_line": 59, + "start_line": 62, + "end_line": 70, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74184,14 +75541,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n" + "body": " // #region deleteConfig [C:2] [TYPE Function]\n // @BRIEF Delete an existing Git server configuration.\n // @PRE configId exists in the backend.\n // @POST Config is removed from the backend.\n async deleteConfig(configId) {\n console.log(`[deleteConfig][Action] Deleting Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'DELETE');\n },\n // #endregion deleteConfig\n" }, { "contract_id": "updateConfig", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 61, - "end_line": 69, + "start_line": 72, + "end_line": 80, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74204,14 +75561,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n" + "body": " // #region updateConfig [C:2] [TYPE Function]\n // @BRIEF Update an existing Git server configuration.\n // @PRE configId exists and configData contains valid fields.\n // @POST Config is updated and returned.\n async updateConfig(configId, config) {\n console.log(`[updateConfig][Action] Updating Git config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}`, 'PUT', config);\n },\n // #endregion updateConfig\n" }, { "contract_id": "testConnection", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 71, - "end_line": 80, + "start_line": 82, + "end_line": 91, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74225,14 +75582,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return requestApi(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n" + "body": " // #region testConnection [C:2] [TYPE Function]\n // @BRIEF Test connection to a Git server with provided credentials.\n // @PRE config contains valid URL and PAT.\n // @POST Returns connection status (success/failure).\n // @DATA_CONTRACT Input(GitServerConfig) -> Output({status: string})\n async testConnection(config) {\n console.log('[testConnection][Action] Testing Git connection');\n return gitRequest(`${API_BASE}/config/test`, 'POST', config);\n },\n // #endregion testConnection\n" }, { "contract_id": "listGiteaRepositories", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 82, - "end_line": 90, + "start_line": 93, + "end_line": 101, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74245,14 +75602,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n" + "body": " // #region listGiteaRepositories [C:2] [TYPE Function]\n // @BRIEF List repositories on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA provider config.\n // @POST Returns array of Gitea repository metadata.\n async listGiteaRepositories(configId) {\n console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`);\n },\n // #endregion listGiteaRepositories\n" }, { "contract_id": "createGiteaRepository", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 92, - "end_line": 100, + "start_line": 103, + "end_line": 111, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74265,14 +75622,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n" + "body": " // #region createGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Create a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; payload has non-empty name.\n // @POST Repository created on Gitea; returns created repo metadata.\n async createGiteaRepository(configId, payload) {\n console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload);\n },\n // #endregion createGiteaRepository\n" }, { "contract_id": "createRemoteRepository", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 102, - "end_line": 111, + "start_line": 113, + "end_line": 122, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74286,14 +75643,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n" + "body": " // #region createRemoteRepository [C:2] [TYPE Function]\n // @BRIEF Create repository on remote provider selected by Git config (idempotent: returns existing on 409).\n // @PRE configId exists and points to supported provider; payload has non-empty name.\n // @POST Remote repository created (or existing returned); normalized payload returned.\n // @DATA_CONTRACT Input(configId, RemoteRepoCreatePayload) -> Output(RemoteRepoSchema)\n async createRemoteRepository(configId, payload) {\n console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/repositories`, 'POST', payload);\n },\n // #endregion createRemoteRepository\n" }, { "contract_id": "deleteGiteaRepository", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 113, - "end_line": 121, + "start_line": 124, + "end_line": 132, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74306,14 +75663,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return requestApi(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n" + "body": " // #region deleteGiteaRepository [C:2] [TYPE Function]\n // @BRIEF Delete a repository on Gitea for a saved Git configuration.\n // @PRE configId references a GITEA config; owner and repoName are non-empty.\n // @POST Repository deleted on Gitea server.\n async deleteGiteaRepository(configId, owner, repoName) {\n console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`);\n return gitRequest(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE');\n },\n // #endregion deleteGiteaRepository\n" }, { "contract_id": "initRepository", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 123, - "end_line": 136, + "start_line": 134, + "end_line": 147, "tier": "TIER_2", "complexity": 3, "metadata": { @@ -74334,14 +75691,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n" + "body": " // #region initRepository [C:3] [TYPE Function]\n // @BRIEF Initialize or clone a Git repository for a dashboard.\n // @PRE dashboardRef is non-empty; configId and remoteUrl are valid; envId required when ref is slug.\n // @POST Repository initialized on the backend; GitRepository record created.\n // @DATA_CONTRACT Input(dashboardRef, configId, remoteUrl, envId?) -> Output({status: string, message: string})\n // @RELATION CALLS -> [ApiModule.requestApi]\n async initRepository(dashboardRef, configId, remoteUrl, envId = null) {\n console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', {\n config_id: configId,\n remote_url: remoteUrl\n });\n },\n // #endregion initRepository\n" }, { "contract_id": "getRepositoryBinding", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 138, - "end_line": 146, + "start_line": 149, + "end_line": 157, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74354,14 +75711,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n" + "body": " // #region getRepositoryBinding [C:2] [TYPE Function]\n // @BRIEF Fetch repository binding metadata (config/provider) for a dashboard.\n // @PRE Repository should be initialized for the dashboard.\n // @POST Returns provider, config_id, remote_url, local_path.\n async getRepositoryBinding(dashboardRef, envId = null) {\n console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId));\n },\n // #endregion getRepositoryBinding\n" }, { "contract_id": "getBranches", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 148, - "end_line": 156, + "start_line": 159, + "end_line": 167, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74374,14 +75731,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n" + "body": " // #region getBranches [C:2] [TYPE Function]\n // @BRIEF List all branches for a dashboard's repository.\n // @PRE Repository must be initialized; envId required when dashboardRef is a slug.\n // @POST Returns array of BranchSchema objects.\n async getBranches(dashboardRef, envId = null) {\n console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId));\n },\n // #endregion getBranches\n" }, { "contract_id": "createBranch", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 158, - "end_line": 169, + "start_line": 169, + "end_line": 180, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74394,14 +75751,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n" + "body": " // #region createBranch [C:2] [TYPE Function]\n // @BRIEF Create a new branch in the dashboard's repository.\n // @PRE Source branch exists; name is non-empty; envId required when ref is slug.\n // @POST New branch is created on the backend.\n async createBranch(dashboardRef, name, fromBranch, envId = null) {\n console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', {\n name,\n from_branch: fromBranch\n });\n },\n // #endregion createBranch\n" }, { "contract_id": "checkoutBranch", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 171, - "end_line": 179, + "start_line": 182, + "end_line": 190, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74414,14 +75771,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n" + "body": " // #region checkoutBranch [C:2] [TYPE Function]\n // @BRIEF Switch the repository to a different branch.\n // @PRE Target branch exists; envId required when ref is slug.\n // @POST Repository HEAD is moved to the target branch.\n async checkoutBranch(dashboardRef, name, envId = null) {\n console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name });\n },\n // #endregion checkoutBranch\n" }, { "contract_id": "commit", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 181, - "end_line": 189, + "start_line": 192, + "end_line": 200, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74434,14 +75791,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n" + "body": " // #region commit [C:2] [TYPE Function]\n // @BRIEF Stage and commit changes to the repository.\n // @PRE message is non-empty; envId required when ref is slug.\n // @POST Changes are committed to the current branch.\n async commit(dashboardRef, message, files, envId = null) {\n console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files });\n },\n // #endregion commit\n" }, { "contract_id": "push", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 191, - "end_line": 199, + "start_line": 202, + "end_line": 210, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74454,14 +75811,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n" + "body": " // #region push [C:2] [TYPE Function]\n // @BRIEF Push local commits to the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Remote is updated with local commits.\n async push(dashboardRef, envId = null) {\n console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST');\n },\n // #endregion push\n" }, { "contract_id": "deleteRepository", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 201, - "end_line": 209, + "start_line": 212, + "end_line": 220, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74474,14 +75831,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n" + "body": " // #region deleteRepository [C:2] [TYPE Function]\n // @BRIEF Delete local repository binding and workspace for a dashboard.\n // @PRE dashboardRef resolves on backend; envId required when ref is slug.\n // @POST Repository record and local folder are removed.\n async deleteRepository(dashboardRef, envId = null) {\n console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE');\n },\n // #endregion deleteRepository\n" }, { "contract_id": "pull", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 211, - "end_line": 219, + "start_line": 222, + "end_line": 230, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74494,14 +75851,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n" + "body": " // #region pull [C:2] [TYPE Function]\n // @BRIEF Pull changes from the remote repository.\n // @PRE Remote 'origin' is configured and accessible; envId required when ref is slug.\n // @POST Local repository is updated with remote changes.\n async pull(dashboardRef, envId = null) {\n console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST');\n },\n // #endregion pull\n" }, { "contract_id": "getMergeStatus", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 221, - "end_line": 229, + "start_line": 232, + "end_line": 240, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74514,14 +75871,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n" + "body": " // #region getMergeStatus [C:2] [TYPE Function]\n // @BRIEF Fetch unfinished-merge status for a repository.\n // @PRE Repository must exist; envId required when ref is slug.\n // @POST Returns merge status payload with has_unfinished_merge flag.\n async getMergeStatus(dashboardRef, envId = null) {\n console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId));\n },\n // #endregion getMergeStatus\n" }, { "contract_id": "getMergeConflicts", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 231, - "end_line": 239, + "start_line": 242, + "end_line": 250, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74534,14 +75891,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n" + "body": " // #region getMergeConflicts [C:2] [TYPE Function]\n // @BRIEF List merge conflicts for a repository with unresolved merge.\n // @PRE Unfinished merge is in progress; envId required when ref is slug.\n // @POST Returns array of conflict file entries with mine/theirs previews.\n async getMergeConflicts(dashboardRef, envId = null) {\n console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId));\n },\n // #endregion getMergeConflicts\n" }, { "contract_id": "resolveMergeConflicts", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 241, - "end_line": 251, + "start_line": 252, + "end_line": 262, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74554,14 +75911,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n" + "body": " // #region resolveMergeConflicts [C:2] [TYPE Function]\n // @BRIEF Apply conflict resolution strategies and stage resolved files.\n // @PRE resolutions array contains file_path and resolution entries; envId required when ref is slug.\n // @POST Conflicts are resolved and staged.\n async resolveMergeConflicts(dashboardRef, resolutions, envId = null) {\n console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', {\n resolutions: Array.isArray(resolutions) ? resolutions : []\n });\n },\n // #endregion resolveMergeConflicts\n" }, { "contract_id": "abortMerge", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 253, - "end_line": 261, + "start_line": 264, + "end_line": 272, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74574,14 +75931,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n" + "body": " // #region abortMerge [C:2] [TYPE Function]\n // @BRIEF Abort current unfinished merge.\n // @PRE Repository exists; envId required when ref is slug.\n // @POST Merge state is aborted or reported as absent.\n async abortMerge(dashboardRef, envId = null) {\n console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST');\n },\n // #endregion abortMerge\n" }, { "contract_id": "continueMerge", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 263, - "end_line": 273, + "start_line": 274, + "end_line": 284, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74594,14 +75951,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n" + "body": " // #region continueMerge [C:2] [TYPE Function]\n // @BRIEF Finalize unfinished merge by creating a merge commit.\n // @PRE All conflicts are resolved; envId required when ref is slug.\n // @POST Merge commit is created.\n async continueMerge(dashboardRef, message = '', envId = null) {\n console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', {\n message: String(message || '').trim() || null\n });\n },\n // #endregion continueMerge\n" }, { "contract_id": "getEnvironments", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 275, - "end_line": 282, + "start_line": 286, + "end_line": 293, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74613,14 +75970,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return requestApi(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n" + "body": " // #region getEnvironments [C:2] [TYPE Function]\n // @BRIEF Fetch available deployment environments from Git config.\n // @POST Returns list of environment objects.\n async getEnvironments() {\n console.log('[getEnvironments][Action] Fetching environments');\n return gitRequest(`${API_BASE}/environments`);\n },\n // #endregion getEnvironments\n" }, { "contract_id": "deploy", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 284, - "end_line": 294, + "start_line": 295, + "end_line": 305, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74633,14 +75990,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n" + "body": " // #region deploy [C:2] [TYPE Function]\n // @BRIEF Deploy a dashboard to a target environment.\n // @PRE Environment must be active and accessible; envId required when ref is slug.\n // @POST Dashboard is imported into target Superset instance.\n async deploy(dashboardRef, environmentId, envId = null) {\n console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', {\n environment_id: environmentId\n });\n },\n // #endregion deploy\n" }, { "contract_id": "getHistory", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 296, - "end_line": 303, + "start_line": 307, + "end_line": 314, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74652,14 +76009,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n" + "body": " // #region getHistory [C:2] [TYPE Function]\n // @BRIEF Fetch commit history for a dashboard repository.\n // @POST Returns list of commits with author, date, message.\n async getHistory(dashboardRef, limit = 50, envId = null) {\n console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId));\n },\n // #endregion getHistory\n" }, { "contract_id": "sync", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 305, - "end_line": 317, + "start_line": 316, + "end_line": 328, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74671,14 +76028,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return requestApi(endpoint, 'POST');\n },\n // #endregion sync\n" + "body": " // #region sync [C:2] [TYPE Function]\n // @BRIEF Synchronize local dashboard state with Git repository.\n // @POST Dashboard state is synced to Git workspace.\n async sync(dashboardRef, sourceEnvId = null, envId = null) {\n console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`);\n const params = new URLSearchParams();\n if (sourceEnvId) params.append('source_env_id', String(sourceEnvId));\n if (envId) params.append('env_id', String(envId));\n const query = params.toString();\n const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`;\n return gitRequest(endpoint, 'POST');\n },\n // #endregion sync\n" }, { "contract_id": "getStatus", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 319, - "end_line": 327, + "start_line": 330, + "end_line": 338, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74691,14 +76048,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n" + "body": " // #region getStatus [C:2] [TYPE Function]\n // @BRIEF Fetch current Git status for a dashboard repository (dirty files, branch info).\n // @PRE Repository path exists on disk; envId required when ref is slug.\n // @POST Returns status payload with is_dirty, staged_files, modified_files, current_branch.\n async getStatus(dashboardRef, envId = null) {\n console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/status', envId));\n },\n // #endregion getStatus\n" }, { "contract_id": "getStatusesBatch", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 329, - "end_line": 339, + "start_line": 340, + "end_line": 350, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74711,14 +76068,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return requestApi(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n" + "body": " // #region getStatusesBatch [C:2] [TYPE Function]\n // @BRIEF Fetch Git statuses for multiple dashboards in one batch request.\n // @PRE dashboardIds is a non-empty array of numeric IDs.\n // @POST Returns a map of dashboard_id -> status payload (with NO_REPO for uninitialized).\n async getStatusesBatch(dashboardIds) {\n console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`);\n return gitRequest(`${API_BASE}/repositories/status/batch`, 'POST', {\n dashboard_ids: dashboardIds\n });\n },\n // #endregion getStatusesBatch\n" }, { "contract_id": "getDiff", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 341, - "end_line": 355, + "start_line": 352, + "end_line": 366, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74731,14 +76088,14 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return requestApi(endpoint);\n },\n // #endregion getDiff\n" + "body": " // #region getDiff [C:2] [TYPE Function]\n // @BRIEF Fetch Git diff for a dashboard repository (staged or unstaged).\n // @PRE Repository path exists; envId required when ref is slug.\n // @POST Returns unified diff string.\n async getDiff(dashboardRef, filePath = null, staged = false, envId = null) {\n console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`);\n let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`;\n const params = new URLSearchParams();\n if (filePath) params.append('file_path', filePath);\n if (staged) params.append('staged', 'true');\n if (envId) params.append('env_id', String(envId));\n if (params.toString()) endpoint += `?${params.toString()}`;\n return gitRequest(endpoint);\n },\n // #endregion getDiff\n" }, { "contract_id": "promote", "contract_type": "Function", "file_path": "frontend/src/services/gitService.js", - "start_line": 357, - "end_line": 365, + "start_line": 368, + "end_line": 376, "tier": "TIER_1", "complexity": 2, "metadata": { @@ -74751,7 +76108,7 @@ "schema_warnings": [], "anchor_syntax": "region", "tier_source": "AutoCalculated", - "body": " // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n" + "body": " // #region promote [C:2] [TYPE Function]\n // @BRIEF Promote changes between branches via MR or direct merge.\n // @PRE Dashboard repository initialized; from_branch and to_branch differ; envId required when ref is slug.\n // @POST Returns MR URL (MR mode) or merge status (direct mode).\n async promote(dashboardRef, payload, envId = null) {\n console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`);\n return gitRequest(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload);\n }\n // #endregion promote\n" }, { "contract_id": "StorageService", @@ -75635,10142 +76992,10177 @@ } ], "file_hashes": { - "venv/lib/python3.13/site-packages/iniconfig/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/uvicorn/__main__.py": 6086938803696646644, - "docs/api/html/group__test__cte__with__schema__tables.html": 5116514654709271953, - "backend/src/core/middleware/trace.py": 10060731108870842086, - "docs/api/html/group__parse__llm__response.html": 3772780281405997991, - "docs/api/html/group____resolve__current__user__git__identity.html": 9945081661871337184, - "venv/lib/python3.13/site-packages/pandas/_testing/contexts.py": 11714205921752896137, - "backend/src/plugins/translate/preview_review.py": 5584592578627646560, - "backend/git_repos/remote/test-repo.git/description": 15262637409357137373, - "venv/lib/python3.13/site-packages/pydantic/experimental/missing_sentinel.py": 1637241331832367709, - "docs/api/html/group__extract__tables__from__sql__span.html": 1554822379773857556, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/util.py": 2866856507760047219, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_subclass.py": 5540948578269213810, - "docs/api/html/group__TranslatePreviewRoutesModule.html": 17344666533968003326, - "backend/tests/services/clean_release/test_publication_service.py": 12656754025482108211, - "backend/src/plugins/translate/orchestrator_sql_rows.py": 13887342178846174274, - "venv/lib/python3.13/site-packages/pydantic/_internal/_internal_dataclass.py": 13206778611516171760, - "frontend/build/_app/immutable/nodes/38.DR2JbkbT.js": 4406964953418448592, - "docs/api/html/search/groups_13.js": 5984573013641066016, - "docs/api/html/group____enforce__dictionary.html": 18173488447816081316, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_functions.py": 2255133375699086300, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/code.py": 9700811903057180696, - "docs/api/html/group__test__end__already__completed__event.html": 5476557657288556633, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/framework_integration.py": 3935346152156572025, - "docs/api/html/group__test__clean__release__tui__v2.html": 12931166045640948787, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_one_public_routine.f90": 7957683453180939605, - "docs/api/html/group__createUser__Function.html": 9916552781618582040, - "docs/api/html/group__AdminApi.html": 767628307398103052, - "venv/lib/python3.13/site-packages/attrs/exceptions.py": 2871247302586505373, - "backend/src/core/superset_client/_dashboards_filters.py": 17625905985676889586, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/requirements.py": 2362917888844255805, - "docs/api/html/group__DetectedIssue.html": 49569046639972610, - "frontend/src/lib/components/dataset-review/CompiledSQLPreview.svelte": 1996807488169542658, - "venv/lib/python3.13/site-packages/cffi/recompiler.py": 17809252277700094304, - "docs/api/html/group__ManifestBuilder.html": 9042925776836812751, - "docs/api/html/group__AuthLoggerModule.html": 4166355749229873269, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_usecols_basic.py": 16382444980424010514, - "backend/src/api/routes/git/_config_routes.py": 9064062564638523731, - "backend/src/services/clean_release/__tests__/test_policy_engine.py": 7872293927249163831, - "docs/api/html/sync_on.png": 14172530649108700754, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/warnings_and_errors.pyi": 3151453262733619734, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append.py": 13152738905565588661, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py": 11225173272050906977, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh27697.f90": 4990763135487995517, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/json.py": 14755215397454910426, - "venv/lib/python3.13/site-packages/attr/_version_info.pyi": 7141309436445210354, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict_of_blocks.py": 11646835537993064324, - "docs/api/html/group__candidate__repository.html": 7211404956141328496, - "docs/api/html/group____require__clarification__flag.html": 7381291501052368444, - "docs/api/html/group__run__compliance__violations.html": 7576622887921005784, - "docs/api/html/group__test__database__url__postgres__fallback.html": 15099231812721342773, - "docs/api/html/group__TranslationRunStore.html": 630348523706519639, - "docs/api/html/group____get__task__service.html": 7652976387374733234, - "venv/lib/python3.13/site-packages/pip/_internal/commands/freeze.py": 8644678595442275485, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/putmask.py": 10934101690788549526, - "venv/lib/python3.13/site-packages/pygments/lexers/snobol.py": 414946539602526513, - "venv/lib/python3.13/site-packages/starlette/testclient.py": 16088848330558215194, - "docs/api/html/group__ManifestRepositoryModule.html": 16790910808753143759, - "venv/lib/python3.13/site-packages/pandas/tests/tools/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/multiarray.py": 3224049574922696945, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_mixins.py": 14293848457775268010, - "venv/lib/python3.13/site-packages/numpy/core/__init__.py": 10397294636564169377, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/common.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_dtypes.py": 6831042272921908068, - "docs/api/html/group__status__to__report__status.html": 9738740379403112359, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/macos.py": 17442393555211213147, - "docs/api/html/group__download__file.html": 17674094830144003140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/operators.py": 5584982192355398255, - "docs/api/html/group__MaintenanceBannerPlugin.html": 17523541172049741190, - "venv/lib/python3.13/site-packages/anyio/_core/_exceptions.py": 11969894261631227715, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/state.py": 10500170103357297551, - "venv/lib/python3.13/site-packages/cffi/pkgconfig.py": 16057517851163939571, - "frontend/src/components/git/CommitHistory.svelte": 6419514228971332714, - "frontend/src/routes/settings/+page.ts": 3719008513710037840, - "venv/lib/python3.13/site-packages/PIL/GimpGradientFile.py": 9777026720028891154, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____getSettings.html": 17142414359252665834, - "docs/api/html/group__test__update__git__config__raises__404__if__not__found.html": 17378932393540768304, - "docs/api/html/group__test__json__load__if__needed__none.html": 16725871875362821962, - "backend/git_repos/remote/test-repo.git/refs/heads/dev": 8596267258668462395, - "docs/api/html/group__frontend__src__lib__api__validation__js____createTask.html": 11277710494667674474, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_c_parser_only.py": 3038797485572319804, - "docs/api/html/group____ensure__git__server__configs__columns.html": 11324045515305585, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot.py": 334735766122314163, - "venv/lib/python3.13/site-packages/pandas/core/construction.py": 10646956189518908985, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/__init__.py": 13331622784664204066, - "docs/api/html/group__test__end__all__returns__202.html": 2143037192454601486, - "docs/api/html/group__seed__superset__load__data.html": 314813090128633817, - "docs/api/html/group__ValidationFindingsPanel.html": 16524458792905973929, - "docs/api/html/search/pages_2.js": 10895953379429449652, - "docs/api/html/group__SupersetAuthCache__get.html": 5371825726490397800, - "docs/api/html/group__getSortStatusValue__Function.html": 16892527846024245299, - "docs/api/html/group__ClarificationAnswer.html": 11735527821059013045, - "docs/api/html/group__load__persisted__tasks.html": 897838907183043727, - "docs/api/html/tab_hd.png": 15081546309524496729, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/traceback.py": 2535227583726271746, - "venv/lib/python3.13/site-packages/numpy/version.py": 1497063540123613501, - "venv/lib/python3.13/site-packages/pandas/core/computation/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py": 14865906347579451546, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_pipe.py": 8255917221095334212, - "docs/api/html/group____cleanup__manager.html": 13234621596823816761, - "venv/lib/python3.13/site-packages/numpy/core/numeric.py": 17025007229779320230, - "venv/lib/python3.13/site-packages/anyio/streams/memory.py": 11762722143503428823, - "docs/api/html/group__ResolutionState.html": 10695599682099168307, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/file_proxy.py": 17695135735211897444, - "docs/api/html/group__start__session.html": 8274556149199075536, - "docs/api/html/group__test__belief__scope__success__coherence.html": 9904691934239125152, - "docs/api/html/group__DEFAULT__LLM__PROVIDER__BINDINGS.html": 1197817568915985055, - "docs/api/html/group__SupersetContextTemplatesMixin.html": 15324522568275459473, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.py": 7446626038222262489, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py": 7111764828004587758, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/meson.build": 17468163147765825687, - "venv/lib/python3.13/site-packages/pygments/lexers/ul4.py": 9580182474451220952, - "docs/api/html/group__Task.html": 4118191587870262013, - "venv/lib/python3.13/site-packages/pydantic/__init__.py": 11536052293866174672, - "venv/lib/python3.13/site-packages/pandas/_libs/pandas_parser.cpython-313-x86_64-linux-gnu.so": 18056339336364727359, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____pollStatus.html": 17320763933447233448, - "docs/api/html/group__encodeStoragePath__Function.html": 13436882047096086584, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/ddl.py": 5006132143435653867, - "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimelike.py": 4356700619632833878, - "frontend/build/_app/immutable/nodes/9.DpzREatp.js": 279253596265401371, - "docs/api/html/group__test__get__dashboard__detail__env__not__found.html": 9982209556180665563, - "docs/api/html/group______Function.html": 11167138341912770463, - "venv/lib/python3.13/site-packages/pygments/lexers/verification.py": 16645885125604874183, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py": 1331438198531942242, - "docs/api/html/group__TaskDrawerTestModule.html": 15361427361837653112, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dot.py": 16876560325283070437, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/serializer.py": 8708767945343850816, - "docs/api/html/group__TranslationMetricsDashboard.html": 15575796997965788450, - "docs/api/html/group__TaskLogPanel.html": 14846603073845295002, - "docs/api/html/group__get__dialect__from__database.html": 988662418075902152, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unique.py": 6234921572794573905, - "docs/api/html/group__DebounceModule.html": 8591276512225370990, - "docs/api/html/globals.html": 8891371690034474264, - "venv/lib/python3.13/site-packages/jeepney/fds.py": 16573439672758641560, - "docs/api/html/group__DictionaryCreate.html": 5296179021197586468, - "docs/api/html/group__PROMPT__BASE__TOKENS.html": 625575964554485020, - "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key.html": 10269943654735165789, - "backend/tests/core/test_defensive_guards.py": 16395624498768711683, - "venv/lib/python3.13/site-packages/smmap/test/test_util.py": 5634593621291240094, - "docs/api/html/group__build__banner__text.html": 9722068729315930641, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd_module.py": 7122050193390298178, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_arrow_compat.py": 17818294260710640443, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__test__parse__datetime__integer.html": 1059776138445462771, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_lib.py": 6721066015604956469, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_na_scalar.py": 13950899907067372014, - "docs/api/html/group__CorrectionCell.html": 13749448967384828012, - "venv/lib/python3.13/site-packages/httpx/_exceptions.py": 18399489130669465891, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending_distributions.pyx": 15302775787158991386, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_both.f90": 13943066317159236257, - "docs/api/html/group__test__existing__test__would__fail.html": 6044563429962222801, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_julian_date.py": 850085757774959003, - "docs/api/html/group__lock__field__semantic.html": 2797198914997806470, - "venv/lib/python3.13/site-packages/pygments/lexers/graphics.py": 13783143864729682444, - "backend/src/core/utils/superset_context_extractor/_pii.py": 15135596230141304567, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_old_ma.py": 18116460138221458026, - "venv/lib/python3.13/site-packages/pygments/lexers/cddl.py": 1303059539145221713, - "docs/api/html/group__GitServiceStatusMixin.html": 4523615132638465854, - "venv/lib/python3.13/site-packages/gitdb/db/pack.py": 13460574575424209609, - "docs/api/html/group__TaskLogViewer.html": 7478297409067123926, - "venv/lib/python3.13/site-packages/PIL/_webp.pyi": 18222325750818585549, - "docs/api/html/group__GitUtils.html": 17815241981515134154, - "venv/lib/python3.13/site-packages/pycparser/plyparser.py": 5251740466238035035, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_datetime64.py": 16272266806084058743, - "venv/lib/python3.13/site-packages/pygments/lexers/console.py": 18266795648198475028, - "frontend/src/lib/api/__tests__/reports_api.test.js": 4759560641984508050, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/__init__.py": 8705044848308260480, - "venv/lib/python3.13/site-packages/dotenv/variables.py": 5438726379749803751, - "frontend/src/lib/components/reports/reportTypeProfiles.js": 10752964062056890725, - "docs/api/html/group__SupersetContextTemplatesMixin__discover__template__variables.html": 1854980804207129510, - "venv/lib/python3.13/site-packages/_pytest/python.py": 1829073958095716309, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/AUTHORS": 3558407083075395848, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polytypes.pyi": 7528777458251688517, - "docs/api/html/group__ClarificationSessionSummaryResponse.html": 6287729552566678011, - "docs/api/html/search/all_16.js": 6930966498952410677, - "backend/src/plugins/translate/orchestrator_run_completion.py": 1246106720048798445, - "docs/api/html/files_dup.js": 6566197532364479572, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_editable.py": 14352954849821931255, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_index.py": 10971093361190283675, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL": 18203500019759199992, - "docs/api/html/group__datasetReviewSession__Store.html": 2454065544502690848, - "backend/src/services/__tests__/test_encryption_manager.py": 11651916791559202800, - "venv/lib/python3.13/site-packages/numpy/core/multiarray.py": 18368443598659711836, - "docs/api/html/group__ADGroupMappingSchema.html": 11869729536945519142, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_free.f90": 8821492401453445757, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/pickleable.py": 7712040860601343075, - "frontend/src/lib/components/layout/__tests__/test_taskDrawer.svelte.js": 7736587487210851851, - "docs/api/html/group__formatSize__Function.html": 8913344485781991642, - "docs/api/html/group__ValidationRoutes.html": 9947226758890063064, - "docs/api/html/group__AuditService.html": 14546698565795749486, - "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.pyi": 15745512451486888537, - "backend/src/plugins/translate/__tests__/test_dictionary.py": 4341966549214032934, - "backend/src/core/superset_client/_datasets.py": 10922216252382553392, - "backend/src/core/superset_client/_dashboards_crud.py": 8682388093872211883, - "frontend/build/_app/immutable/chunks/0Mp-5rTH.js": 10539257590394310220, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcom_err-2abe824b.so.2.1": 12870528025628871295, - "docs/api/html/group__LoggingConfig.html": 5718768754954658081, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/coercions.py": 5112358449376598662, - "docs/api/html/group__DashboardChartItem.html": 6833781467610303476, - "docs/api/html/group__APIClient__authenticate.html": 3524465342974233149, - "docs/api/html/group__get__sources.html": 16601137258119600037, - "docs/api/html/group__test__get__dashboards__with__search.html": 9037019464046043095, - "docs/api/html/group__test__override__root__path.html": 18363961162670447666, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_concat.py": 1351478313334379336, - "venv/lib/python3.13/site-packages/pygments/lexers/archetype.py": 8995143180482517233, - "docs/api/html/group__TranslationStageRunner.html": 5118717378303155773, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/licenses/COPYING": 14459782230785388765, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asfreq.py": 2701081989348180239, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/RECORD": 16558021503346615951, - "venv/lib/python3.13/site-packages/pycparser/ply/lex.py": 3393278208465358140, - "venv/bin/jsonschema": 16516374846693491442, - "docs/api/html/group__test__key__without__end__time.html": 1016524169546718308, - "dist/docker/backend.0.1.2.tar.xz": 16563303263172488254, - "backend/src/core/task_manager/__tests__/test_task_logger.py": 2358230637213309608, - "venv/bin/pip3": 13861749540792881808, - "docs/api/html/group__test__dashboard__health__item__status__accepts__only__whitelisted__values.html": 136153182983638913, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_constructors.py": 12628643543371952431, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_iteration.py": 15247340610412364508, - "docs/api/html/group__start__scheduler.html": 5136832911990580041, - "frontend/build/_app/immutable/nodes/28.BuG2LCbC.js": 13515061122874684102, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_query_eval.py": 17613800806748649144, - "docs/api/html/group__test__manifest__build__contract.html": 6734412557509934205, - "docs/api/html/group__test__belief__scope__reason__not__visible__at__info.html": 4840136292915497368, - "venv/lib/python3.13/site-packages/numpy/_core/_asarray.pyi": 3940937389249665490, - "docs/api/html/group__TestMappingService.html": 11609065687830081419, - "docs/api/html/group__test__create__job__missing__translation__column.html": 9558284213073049757, - "docs/api/html/group__test__tui__clear__history__f7.html": 2956037542999638884, - "docs/api/html/group__reject__candidate.html": 4888870338363798729, - "docs/api/html/group__SupersetAPIError.html": 5887592897373484111, - "frontend/src/lib/components/layout/Sidebar.svelte": 18409661059413656763, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.py": 6617116180936986481, - "venv/lib/python3.13/site-packages/pycparser/ply/cpp.py": 1665654017894222868, - "venv/lib/python3.13/site-packages/passlib/utils/binary.py": 13123975436674239306, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata.py": 2021612995853910808, - "backend/src/plugins/translate/events.py": 16974204038587054475, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_localize.py": 18409231586451494605, - "docs/api/html/group__DatabaseMappingsResponse.html": 13473207819975423910, - "venv/lib/python3.13/site-packages/ecdsa/test_eddsa.py": 1785854649532849946, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_character.py": 17168707712220699738, - "docs/api/html/group__SessionEventLogger.html": 3346105078868199944, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed448.py": 12264234876933578367, - "docs/api/html/group__stop.html": 16956058968433395845, - "docs/api/html/group__test__load__config__syncs__environment__records__from__existing__db__payload.html": 5104582144704294884, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____stopTranslationRun.html": 2429083995964044538, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/RECORD": 2688648742701423549, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_reindex.py": 15794750188594767026, - "docs/api/html/group__FuzzyMatching.html": 7818943824809763950, - "frontend/src/routes/translate/history/+page.svelte": 9504117584329454876, - "docs/api/html/group__HealthCenterPage.html": 18169365959978788379, - "frontend/vitest.config.js": 4756170101280085905, - "venv/lib/python3.13/site-packages/pandas/core/reshape/concat.py": 2218848863149192467, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.pyi": 2700568594438441246, - "docs/api/html/group__activity__Store.html": 10103402141887655434, - "backend/src/api/routes/migration.py": 16089356899043357784, - "docs/api/html/group__CompiledPreviewDto.html": 13008981899007160436, - "docs/api/html/group__DashboardHub__loadDashboards__Function.html": 3241502516261310568, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/__init__.py": 2791238481813240414, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/claims.py": 14794441479740527265, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/jws_eddsa.py": 833927568269372333, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2": 6199424505696679197, - "venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py": 15244280495983216204, - "docs/api/html/group__ClarificationSession.html": 5830007380019646698, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/color.py": 15062855556511243610, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py": 7624316342162916928, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____listEvents.html": 2536379713149954797, - "docs/api/html/group__JobLifecycleModule.html": 9294657013529103257, - "docs/api/html/group__test__toggleSidebar__Function.html": 11685985194827077966, - "docs/api/html/group__CleanReleaseV2Api.html": 4541756624375891819, - "venv/lib/python3.13/site-packages/cffi/setuptools_ext.py": 30796527302561458, - "docs/api/html/group__SidebarIntegrationTest__Module.html": 17744089971418699905, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_abc.py": 10164280045317807365, - "docs/api/html/group__end__maintenance.html": 13575739036759149752, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_join.py": 1697622393195784944, - "docs/api/html/group__test__json__load__if__needed__plain__string.html": 16000096952478615435, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py": 14576109242055156193, - "venv/lib/python3.13/site-packages/pip/_internal/configuration.py": 2563100675379605810, - "docs/api/html/group__test__execution__snapshot__preserves__mapped__template__variables__and__filter__context.html": 9629082711066592094, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraypad.py": 4907193085512342162, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_replace.py": 8165088653532047247, - "docs/api/html/splitbard.png": 7867562212225872131, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.pyi": 16372699564794732463, - "docs/api/html/group__RepoStatusBatchRequest.html": 4909634255465475833, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/common.py": 9354442374897565498, - "venv/lib/python3.13/site-packages/rapidfuzz/utils.py": 5682627611354455144, - "docs/api/html/group__test__get__logs__with__filters.html": 8248101620464596303, - "venv/lib/python3.13/site-packages/greenlet/PyGreenletUnswitchable.cpp": 3160733517193315684, - "docs/api/html/group__test__sync__environment__upserts__correctly.html": 13332214453849370730, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/exceptions.py": 14982764273485726199, - "venv/lib/python3.13/site-packages/starlette/convertors.py": 17839747154274354605, - "docs/api/html/group__SchemasPackage.html": 18430871611068960110, - "venv/lib/python3.13/site-packages/numpy/random/_common.cpython-313-x86_64-linux-gnu.so": 12834724130890991478, - "docs/api/html/group__TestRegressionGuard.html": 15882303187698666439, - "docs/api/html/group__MaintenanceRouter.html": 16070342961663373297, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_deprecations.py": 16367195310156359251, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_size.py": 12449835577388069407, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_scalar.py": 2434773821902319678, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py": 9224234539232195189, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/pyproject.py": 12684373765476932108, - "venv/lib/python3.13/site-packages/PIL/CurImagePlugin.py": 17697303205012181529, - "venv/lib/python3.13/site-packages/git/__init__.py": 15867730798565725003, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npz": 12369168483236619421, - "venv/lib/python3.13/site-packages/pygments/styles/colorful.py": 15708848112363953394, - "venv/lib/python3.13/site-packages/pandas/_libs/window/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/vyper.py": 6477559246065994199, - "frontend/build/_app/immutable/nodes/8.qFUsBXPN.js": 1171125291889808439, - "frontend/build/_app/immutable/chunks/DNOraAy0.js": 868732761380476466, - "backend/src/plugins/translate/orchestrator_validation.py": 17464651621865127561, - "docs/api/html/group__DashboardScope.html": 315673707971698397, - "docs/api/html/group__persist__task.html": 17252468204924884629, - "docs/api/html/group__SupersetCompilationAdapter____normalize__preview__response.html": 18275343522306944195, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename.py": 4890779493250245903, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_with_comments.f": 3980077223827767077, - "backend/src/core/superset_client/_user_projection.py": 15240732089847883635, - "docs/api/html/group__preview__prompt__helpers.html": 17763275894621267369, - "docs/api/html/group__StorageSettings.html": 7194021643193109279, - "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/__init__.py": 11709551233929312136, - "venv/lib/python3.13/site-packages/pygments/lexers/tls.py": 3552078393009383412, - "docs/api/html/group____make__manager.html": 16413916660006283470, - "frontend/src/components/llm/ProviderConfig.svelte": 18108375341352259203, - "docs/api/html/group__resetDataTimeout__Function.html": 1556809234079451125, - "docs/api/html/group__DatasetReviewWorkspaceEntry.html": 10861049542091300011, - "frontend/src/lib/components/translate/TermCorrectionPopup.svelte": 2290415616531498089, - "docs/api/html/group__triggerRun__Function.html": 981175398640370870, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_complex.py": 4290059580478881700, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_limited_api.py": 15788485008764560845, - "venv/lib/python3.13/site-packages/fastapi/security/utils.py": 2616633862993539558, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/__init__.py": 15644105878811738516, - "docker/nginx.conf": 14595959129793115016, - "docs/api/html/group__SupersetClientDeleteDashboard.html": 11929736219574951751, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.cpython-313-x86_64-linux-gnu.so": 2311012065007769341, - "venv/lib/python3.13/site-packages/git/objects/blob.py": 8972374723153211283, - "docs/api/html/group__RunHistoryFilter.html": 14147174199498703539, - "venv/lib/python3.13/site-packages/pydantic_settings/main.py": 17272314000918984790, - "docs/api/html/group__MigrationMappingsPage.html": 17363977661933102131, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/RECORD": 9502524172853437600, - "venv/lib/python3.13/site-packages/dotenv/ipython.py": 15904966766060247908, - "venv/lib/python3.13/site-packages/PIL/ImtImagePlugin.py": 17599267755504817169, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/__init__.py": 635200170992032966, - "docs/api/html/group__test__parse__dashboard__url__for__filters__no__filters.html": 2766201645946091724, - "docs/api/html/group__CleanReleaseConfig.html": 5411302572007077971, - "docs/api/html/group__test__sql__table__extractor.html": 10962497480453520163, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py": 6109201994963211857, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_crosstab.py": 6631711416347033281, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_pbkdf2.py": 4977067068389993374, - "venv/lib/python3.13/site-packages/dateutil/tzwin.py": 5149446703746204349, - "docs/api/html/group__test__sync__network__404__mapping__keeps__non__dashboard__endpoints__generic.html": 12571154995084509267, - "docs/api/html/search/groups_c.js": 3976149661731985741, - "docs/api/html/group__test__start__returns__202.html": 4326452274740298704, - "frontend/src/pages/Settings.svelte": 14062925006358678049, - "docs/api/html/group__HealthPageIntegrationTest__Module.html": 8263116403958014254, - "docs/api/html/group__test__create__api__key.html": 9726291706125777558, - "docs/api/html/group____init__session.html": 12506025499557216543, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py": 2300277198409164253, - "docs/api/html/group__test__checking__state.html": 1723457618410256625, - "docs/api/html/group__TriggerRunResponse.html": 2460487645521789314, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/authorization_server.py": 17967809814827547139, - "venv/lib/python3.13/site-packages/pydantic/functional_validators.py": 5835339743072048940, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarmath.py": 9762951701352556447, - "venv/lib/python3.13/site-packages/pandas/io/orc.py": 12754290195886557668, - "docs/api/html/group__MAX__OUTPUT__HEADROOM.html": 8679042542420488178, - "venv/lib/python3.13/site-packages/pandas/io/parsers/base_parser.py": 15148258424429418380, - "docs/api/html/group__OUTPUT__PER__ROW__PER__LANG.html": 8649435613507294987, - "venv/lib/python3.13/site-packages/attrs/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/comparisons.pyi": 9338084426044982038, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_year.py": 17441853503700184299, - "venv/lib/python3.13/site-packages/PIL/_version.py": 3025206805667405876, - "docs/api/html/group__StateMock.html": 14819043532305148003, - "docs/api/html/group__test__preview__prefers__supported__client__method__before__network__fallback.html": 4750969353248108687, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_setops.py": 8622608042929141013, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_cat.py": 15988884721768567895, - "venv/lib/python3.13/site-packages/pyasn1/type/tagmap.py": 14075859042659927673, - "docs/api/html/group__test__delete__task__not__found.html": 11432183556776181332, - "venv/lib/python3.13/site-packages/jose/backends/rsa_backend.py": 13507186353881931758, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/__init__.py": 13749066369790839334, - "venv/lib/python3.13/site-packages/numpy/typing/__init__.py": 3608665862759745048, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_copy.py": 3114918520437707410, - "docs/api/html/group__get__repository__diff.html": 13880615963280114333, - "docs/api/html/group____sync__environment__records.html": 7144329195517588362, - "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.pyi": 11310761736182213265, - "docs/api/html/group____get__dictionary__entry__counts.html": 15029433129373486518, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.cpython-313-x86_64-linux-gnu.so": 12776951671822017500, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/METADATA": 7727489762869155085, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/libs/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libldap-2974a1ba.so.2.0.200": 13590569080612602445, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90": 7589258200398878529, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_groupby.py": 15721254196883995634, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/encoder.py": 4790267438222037867, - "backend/src/core/task_manager/__init__.py": 15991802770215124472, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_map.py": 6556132946198785761, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_constructors.py": 16197656939536784142, - "docs/api/html/group____matches__dashboard__actor__aliases.html": 12171503296402923897, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_comparison.py": 5348781442514871089, - "venv/lib/python3.13/site-packages/httpcore/_trace.py": 9576253018891841832, - "backend/tests/services/clean_release/test_demo_mode_isolation.py": 15017454272480710246, - "docs/api/html/group__estimate__token__budget.html": 8041209033225986920, - "docs/api/html/group__get__repository__status__batch.html": 5326587489507383540, - "venv/lib/python3.13/site-packages/passlib/handlers/digests.py": 18158237109145814857, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/WHEEL": 1598805043198466373, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_value_counts.py": 9352540710376306520, - "venv/lib/python3.13/site-packages/charset_normalizer/cli/__init__.py": 9702005609966176016, - "venv/lib/python3.13/site-packages/pygments/lexers/yara.py": 17510792615476493957, - "backend/src/api/routes/git/_repo_operations_routes.py": 11164792016063890809, - "backend/git_repos/remote/test-repo.git/objects/6f/85e20ce3ef98cf07a2f102498b71b9462552c5": 3012017807908986452, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/request.py": 10492090878715126550, - "venv/lib/python3.13/site-packages/_pytest/mark/structures.py": 12237146603608917650, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein_py.py": 1933257612725836484, - "venv/lib/python3.13/site-packages/pandas/tests/window/conftest.py": 10673264126684571493, - "docs/api/html/group__frontend__src__services__git__utils__js____extractHttpHost.html": 17505817962163768324, - "venv/lib/python3.13/site-packages/dotenv/__init__.py": 15464030923609356017, - "venv/lib/python3.13/site-packages/numpy/_typing/_array_like.py": 2161873439361281488, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/util.py": 6088868370298560146, - "venv/lib/python3.13/site-packages/pygments/styles/inkpot.py": 10516710471319090846, - "docs/api/html/group__ScheduleConfig.html": 11162916934238527143, - "backend/src/core/utils/superset_context_extractor/__init__.py": 16372223806424875537, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarprint.py": 9482100952562512933, - "frontend/src/lib/components/translate/TranslationMetricsDashboard.svelte": 8577281635452165836, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh22648.pyf": 15094859289098837494, - "frontend/build/_app/immutable/chunks/B0K5kI6n.js": 6208436857520817685, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/basereload.py": 14479857245734482609, - "venv/lib/python3.13/site-packages/keyring/backends/kwallet.py": 3071915635647280308, - "docs/api/html/group__TaskLogViewerTest__Module.html": 3664733466330383169, - "venv/lib/python3.13/site-packages/anyio/to_process.py": 16627578499791368365, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeLlmSettings.html": 11184309114880759934, - "venv/bin/Activate.ps1": 1196103427939049710, - "frontend/build/_app/immutable/nodes/25.CEgu5Uwc.js": 2456525993732594619, - "docs/api/html/group____trigger__backup.html": 17397691449018850685, - "docs/api/html/group__test__key__hash__unique.html": 5562302720760720176, - "docs/api/html/group__test__get__dashboard__tasks__history__filters__success.html": 6489322718230741980, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/color_triplet.py": 17264298271771184, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-2.csv": 14641856424618845798, - "docs/api/html/group____fetch__batch__records.html": 733170840838093592, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/response.py": 353446217435156470, - "venv/lib/python3.13/site-packages/pygments/lexers/theorem.py": 15666523910367201285, - "venv/lib/python3.13/site-packages/passlib/pwd.py": 14727089240749133060, - "docs/api/html/search/groups_9.js": 11508513771310591301, - "frontend/tests/maintenance-store.test.ts": 7211673236234440419, - "backend/git_repos/remote/test-repo.git/hooks/update.sample": 8204581436750313444, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py": 9925183787411796679, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadSettings.html": 10425411179983087921, - "docs/api/html/search/mag.svg": 12711116757118699539, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/models.py": 11521700876708601977, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/dictionary.py": 9599439750713575741, - "docs/api/html/group____serialize__preview.html": 9415726163648130901, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE": 4274653168153720331, - "docs/api/html/group__rebuild__banner.html": 11712513970127576620, - "venv/lib/python3.13/site-packages/PIL/_avif.cpython-313-x86_64-linux-gnu.so": 5243449968857258659, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.pyi": 13068936038543812082, - "docs/api/html/group__frontend__src__lib__api__validation__js____deleteRun.html": 1830404720177429055, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildWorkspaceLaunchBlockers.html": 15003605714654482517, - "backend/tests/test_translate_executor_filter.py": 11717560360659156703, - "docs/api/html/group__ActivityStore.html": 16046706190501806767, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/WHEEL": 5076688779572520166, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_legacy.py": 12735901522735836481, - "docs/api/html/group__GitServiceRemoteMixin.html": 16511670561438840755, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/__init__.py": 12911374670789466961, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ufuncobject.h": 5112456258078662871, - "docs/api/html/group__test__missing__task__id.html": 366377427514445618, - "backend/_convert_defs.py": 208535643309836247, - "docs/api/html/group__fetch__source__rows.html": 5710393657520357008, - "docs/api/html/search/groups_6.js": 13269109705568320590, - "backend/src/api/routes/admin.py": 14496518311272057274, - "docs/api/html/group__TranslationSchedule.html": 9479348771909724895, - "docs/api/html/group__test__baseline__expired__full__mode.html": 2044862420922151303, - "venv/lib/python3.13/site-packages/fastapi/exception_handlers.py": 9907950997607808699, - "docs/api/html/group__TestClickHouseInsertSqlGeneration.html": 10830522054938082095, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/min_max_.py": 13457611930368041821, - "frontend/build/_app/immutable/nodes/37.CSQu-kNz.js": 5339990103822194529, - "docs/api/html/group__test__manifest__service__rejects__missing__candidate.html": 17562104709800078179, - "docs/api/html/group__list__conversations.html": 9235216586392560543, - "venv/lib/python3.13/site-packages/cffi/verifier.py": 8473599757770867754, - "docs/api/html/group__ToastsModule.html": 15194540860991792583, - "docs/api/html/group__DashboardHub__handleTemporaryShowAll__Function.html": 4145528349117048216, - "docs/api/html/group__DataPurityStage.html": 13800816093664302434, - "docs/api/html/group__normalize__whitespace.html": 7327563330573824911, - "docs/api/html/group__GitManager.html": 13687479863451466340, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/adapters.py": 2224938661429637508, - "docs/api/html/group____task__matches__dashboard.html": 12761291922272569962, - "frontend/src/lib/ui/Select.svelte": 1377129595022044035, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/api.py": 12339139862411341381, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/diagnose.py": 17275977405061138181, - "backend/src/core/middleware/__init__.py": 13917394130915533209, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/random.py": 12722750410117529986, - "docs/api/html/group__ReleaseCandidateStatus.html": 5853358063003918750, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_scrypt.py": 8106466515670064013, - "venv/lib/python3.13/site-packages/websockets/legacy/http.py": 16266924390281399720, - "docs/api/html/group__OrchestratorCommands.html": 4670264817515095687, - "venv/lib/python3.13/site-packages/jaraco/context/__init__.py": 10975097417056278427, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tz_localize.py": 13699016868476153393, - "venv/lib/python3.13/site-packages/pygments/styles/autumn.py": 8750937312198920971, - "docs/api/html/group__test__audit__hooks__emit__append__only__event__stream.html": 17318960105938087804, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_normalize.py": 7378656958631172603, - "frontend/src/lib/components/translate/CorrectionCell.svelte": 17398304999652244378, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_frame_equal.py": 12459816298158655274, - "venv/lib/python3.13/site-packages/pygments/lexers/pointless.py": 17552020039609007881, - "backend/tests/test_auth.py": 18059959539697963472, - "venv/lib/python3.13/site-packages/apscheduler/triggers/combining.py": 1264860868414723829, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_iceberg.py": 1726421917529457420, - "docs/api/html/group__test__list__with__keys.html": 10936269121502713168, - "docs/api/html/group____parse__args.html": 15401654965875624913, - "venv/lib/python3.13/site-packages/pip/_internal/utils/compat.py": 1841193561752758336, - "venv/lib/python3.13/site-packages/pandas/io/json/_json.py": 1393095374713367445, - "venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py": 932468678264370661, - "docs/api/html/group__middleware__package.html": 12440510290894977924, - "docs/api/html/group____PluginLoaderStub.html": 11751810125582050067, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odf.py": 15484940480924122966, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_io.py": 4199750143323741326, - "docs/api/html/group__DashboardValidationPlugin.html": 4604274485935435783, - "docs/api/html/group__TargetSchemaApiTest.html": 17699774859084481912, - "venv/lib/python3.13/site-packages/fastapi/openapi/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/base.py": 13269563625127294513, - "docs/api/html/globals_func_u.html": 9477828494895601314, - "venv/lib/python3.13/site-packages/pytest_asyncio/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/ma/mrecords.pyi": 8945514863366384484, - "docs/api/html/group__SeedPermissionsScript.html": 3283052414462232547, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_replace.py": 18039668944453454689, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/pydantic_settings/exceptions.py": 18300876747412267999, - "venv/lib/python3.13/site-packages/pydantic/v1/schema.py": 10008526732895436361, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_overlaps.py": 8287716909793575347, - "backend/src/scripts/init_auth_db.py": 8910351541713877041, - "docs/api/html/group__test__submit__correction__conflict__detected.html": 13261203944442671395, - "venv/lib/python3.13/site-packages/yaml/cyaml.py": 9157893742065554693, - "docs/api/html/group__FieldSemanticUpdateRequest.html": 7457072171844274462, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.pyi": 7410507092040239447, - "backend/src/api/routes/tasks.py": 13688847871690186483, - "docs/api/html/group__test__mixed__jinja__and__sql.html": 6942646961625701929, - "venv/lib/python3.13/site-packages/_pytest/_code/source.py": 9488112979192923739, - "docs/api/html/group__SettingsRouter.html": 16627647098664476974, - "docs/api/html/group__test__recent__pending__run__not__stale.html": 3405692392212130999, - "venv/lib/python3.13/site-packages/pandas/tests/apply/__init__.py": 15130871412783076140, - "docs/api/html/group__filterDashboardsWithRepositories__Function.html": 3751486221728345447, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_indexing.py": 13703573544185051910, - "docs/api/html/group__ValidationTaskCreate.html": 6710870961962534726, - "docs/api/html/group__CONVERSATIONS.html": 17013475999409508111, - "docs/api/html/group__test__api__list__jobs.html": 9050185050148987988, - "venv/lib/python3.13/site-packages/pandas/core/indexes/period.py": 1181348384346239378, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_tools.py": 13994164937603424236, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_indexing.py": 14717641281147152159, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_types.py": 4083297168618981717, - "venv/lib/python3.13/site-packages/charset_normalizer/md.py": 2029760700380476138, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/wait.py": 6196757964348666082, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_to_timestamp.py": 17572983799929378095, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/auto.py": 12818217385551437286, - "backend/conftest.py": 16521922883434363388, - "docs/api/html/group__loadData__Function.html": 8139473336432513519, - "frontend/src/routes/datasets/[id]/+page.svelte": 7539252327680602221, - "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.py": 2490579175967301457, - "docs/api/html/group__ProfileService.html": 6914047737105070955, - "venv/lib/python3.13/site-packages/greenlet/greenlet_compiler_compat.hpp": 465436185632243991, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_hashtable.py": 2620207240930665390, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_values.py": 12064896795288302730, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py": 17221368219485351394, - "docs/api/html/group__DashboardHub__getSortValue__Function.html": 10869826115392569762, - "venv/lib/python3.13/site-packages/gitdb/__init__.py": 15381521749253695923, - "docs/api/html/group____make__add__log__callback.html": 1369040010068771538, - "venv/lib/python3.13/site-packages/pip/_internal/models/search_scope.py": 9282393652860589543, - "docs/api/html/group__list__sessions.html": 4853566056432919079, - "venv/lib/python3.13/site-packages/packaging/_parser.py": 13394093358675095309, - "venv/lib/python3.13/site-packages/requests/__init__.py": 2604491098990438229, - "docs/api/html/group__get__mapping__service.html": 10008745365667639243, - "docs/api/html/group__TargetSchemaValidation.html": 4385079790289560163, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.pyi": 2101854548535776875, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_sorted.py": 1899608688401238388, - "docs/api/html/group__set__trace__id.html": 10194237527174132581, - "docs/api/html/group__TranslationRunLanguageStatsResponse.html": 13012330439222660734, - "venv/lib/python3.13/site-packages/certifi/py.typed": 15130871412783076140, - "docs/api/html/group__remove__tasks.html": 11302233214919049269, - "backend/src/core/async_superset_client.py": 339440171038902146, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py": 4559483361564472117, - "docs/api/html/group__EnvironmentMock.html": 17907946510124954226, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_formats.py": 13500155167624433622, - "docs/api/html/group__test__revoke__valid.html": 15486457788430868004, - "docs/api/html/group__get__task__logs.html": 5609188290893457831, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/apps.py": 14151733532064278336, - "venv/lib/python3.13/site-packages/uvicorn/py.typed": 15240312484046875203, - "docs/api/html/closed.png": 16898092673784447083, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_mrecords.py": 15952147778503828707, - "backend/src/services/mapping_service.py": 14740129302452546978, - "backend/tests/test_task_persistence.py": 13658802670129434578, - "docs/api/html/group__test__generate__docs__env__not__found.html": 10999514980476456328, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/METADATA": 13428590545328703138, - "venv/lib/python3.13/site-packages/pydantic/deprecated/class_validators.py": 2200511838255373813, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_is_monotonic.py": 12899848089975919515, - "docs/api/html/group__sendAssistantMessage__Function.html": 2736224334042274893, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/__init__.py": 16146480198665866916, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.pyi": 13083543811193933305, - "docs/api/html/group__test__composite__keys.html": 3672461709156266376, - "venv/lib/python3.13/site-packages/git/config.py": 1724894144987553201, - "docs/api/html/group__test__api__error.html": 8524046232458013902, - "venv/lib/python3.13/site-packages/httpx/_transports/wsgi.py": 11981127094916412794, - "docs/api/html/group__test__configure__logger__post__conditions.html": 10707062206466040317, - "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.pyi": 9935587108964614958, - "venv/lib/python3.13/site-packages/pygments/lexers/smithy.py": 10789533877863290582, - "docs/api/html/group__selectTask__Function.html": 13172211009893761565, - "venv/lib/python3.13/site-packages/websockets/legacy/framing.py": 12759022769700230409, - "docs/api/html/group__BackupTypes.html": 10757767469167289047, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/utils.py": 9959199071030996679, - "venv/lib/python3.13/site-packages/cryptography/hazmat/_oid.py": 755042927629281577, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/gcp.py": 12328409672947966867, - "venv/lib/python3.13/site-packages/idna/package_data.py": 6583909243002417298, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77fixedform.f95": 10898637805043867370, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_constructors.py": 7466113171256856591, - "backend/tests/test_security_orthogonal.py": 17356198010201191448, - "venv/lib/python3.13/site-packages/pygments/styles/default.py": 13992125975366109635, - "docs/api/html/group__frontend__src__services__storageService__js____uploadFile.html": 16382303397706812434, - "frontend/src/components/storage/FileUpload.svelte": 14890211974307083938, - "docs/api/html/group__update__my__preference.html": 13895164481872930227, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/type_check.pyi": 9177256991985898824, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_quoted_character.py": 2316632434571797367, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py": 12245524932757110911, - "docs/api/html/group__test__except__pass__replaced__with__log.html": 2189835448945118208, - "venv/lib/python3.13/site-packages/_pytest/_io/wcwidth.py": 13515196388826152383, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_function.py": 7048185308307653546, - "frontend/src/lib/stores/health.js": 3621667160018048028, - "backend/src/services/clean_release/repositories/candidate_repository.py": 1901083195512401226, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/checks.pyx": 13187505236986855177, - "venv/lib/python3.13/site-packages/passlib/tests/utils.py": 15403198680231294263, - "backend/src/api/routes/git/__init__.py": 12363460647192569481, - "docs/api/html/group__test__backup__category__unchanged.html": 13517270357463076954, - "docs/api/html/search/functions_2.js": 2385164718268290961, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_read_fwf.py": 13684564756628524738, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/dml.py": 6519732236745017861, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_interpolate.py": 7354685880215444946, - "venv/lib/python3.13/site-packages/fastapi/_compat/may_v1.py": 9880083742927480960, - "docs/api/html/group__AppConfigRecord.html": 6504206258479760211, - "docs/api/html/group__test__blocked__run__finalization__blocks__report__builder.html": 10456245679320672835, - "docs/api/html/group__SidebarStore.html": 9185421740800209773, - "venv/lib/python3.13/site-packages/_pytest/pastebin.py": 13946273207624603063, - "docs/api/html/group__FilterState.html": 14518206927080029563, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/METADATA": 3448868793744358515, - "venv/lib/python3.13/site-packages/packaging/utils.py": 3469957615920103670, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_struct_accessor.py": 6567451952835390075, - "backend/src/services/maintenance/_orchestrators.py": 995360865260101856, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_parse_dates.py": 7597935994945336408, - "venv/lib/python3.13/site-packages/pydantic_core/core_schema.py": 8106298745123589494, - "docs/api/html/group__test__list__runs__filter__job__id.html": 7845039968622069882, - "venv/lib/python3.13/site-packages/PIL/TarIO.py": 13651401276968618561, - "venv/lib/python3.13/site-packages/pydantic/class_validators.py": 4643597082531287188, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/warnings.py": 2552021607611112605, - "venv/lib/python3.13/site-packages/httpx/_decoders.py": 14478643473006178492, - "docs/api/html/group__ClarificationQuestionPayload.html": 13225683527391113096, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE.PSF": 13208428090829817329, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER": 17282701611721059870, - "frontend/src/lib/components/MaintenanceSettingsPanel.svelte": 8672462648876875400, - "venv/lib/python3.13/site-packages/numpy/random/_common.pxd": 3402284125943848851, - "venv/lib/python3.13/site-packages/PIL/MspImagePlugin.py": 3034490726971829439, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/isocintrin/isoCtests.f90": 6208802899442270399, - "docs/api/html/group__save__preview.html": 10710352658491088679, - "docs/api/html/group__test__delete__schedule.html": 16032542899335467172, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/default_comparator.py": 47118577003622736, - "frontend/e2e/tests/enterprise-clean-setup.e2e.js": 6434855878266981931, - "docs/api/html/group__PreviewRowUpdate.html": 17634414564160299335, - "venv/lib/python3.13/site-packages/pandas/_libs/tslib.pyi": 16170606956605100142, - "backend/src/plugins/translate/superset_executor.py": 5480256123348033488, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_item.py": 17619314877770917080, - "docs/api/html/group__TestConfigManagerDefaults.html": 5818758037140085192, - "venv/lib/python3.13/site-packages/websockets/datastructures.py": 5180442040351387281, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_string.py": 10141066226714805009, - "venv/lib/python3.13/site-packages/numpy/_core/printoptions.pyi": 16758392990920041462, - "docs/api/html/group____make__us2__session.html": 6547387069315862625, - "venv/lib/python3.13/site-packages/numpy/lib/introspect.py": 5706729538356302335, - "docs/api/html/group__Icon.html": 13262643427544485857, - "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.pyi": 12052895927933479005, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA": 6656194161182818439, - "frontend/src/lib/api/translate.js": 18140498250896378509, - "docs/api/html/group__PageHeader.html": 5497869529552626290, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_pickle.py": 4216445955812104014, - "docs/api/html/group__cancel__run.html": 14246191788643114054, - "docs/api/html/group__SupersetDatasetsMixin.html": 1567821220831285583, - "venv/lib/python3.13/site-packages/numpy/random/_pcg64.cpython-313-x86_64-linux-gnu.so": 5759445712745875888, - "venv/lib/python3.13/site-packages/rsa/cli.py": 1753949198467694190, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/securetransport.py": 12104888897121018676, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/aws.py": 1364297790170663941, - "docs/api/html/group__ValidationService.html": 18408866395625101302, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_chebyshev.py": 8054411443686791426, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_index_as_string.py": 13421551616136437100, - "frontend/build/_app/immutable/chunks/2dOUpm6k.js": 11811637653151731489, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py": 2492962412506082234, - "docs/api/html/group__TaskDrawerTest__Module.html": 14203440361140799361, - "docs/api/html/group__orchestrator__query.html": 186936932358706266, - "docs/api/html/group__test__sync__environment__handles__api__error__gracefully.html": 3531439474218526807, - "docs/api/html/group____register__plugin.html": 4747901438274521925, - "docs/api/html/group__test__enable__belief__state__flag.html": 9288819795406438186, - "docs/api/html/group__normalizeEnvStage__Function.html": 702224648569356204, - "backend/src/plugins/storage/__init__.py": 5046554941488079204, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo77.f": 16250175448122328648, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/WHEEL": 9100692507274722091, - "docs/api/html/group__ReactiveDashboardFetch__Block.html": 2051150487389286262, - "venv/lib/python3.13/site-packages/fastapi/temp_pydantic_v1_params.py": 9436036880518047720, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sin.csv": 1443087417808027268, - "venv/lib/python3.13/site-packages/numpy/_utils/__init__.pyi": 13530522898409268883, - "backend/src/scripts/clean_release_tui.py": 3006670970245009659, - "venv/lib/python3.13/site-packages/authlib/jose/util.py": 14843923624043422219, - "docs/api/html/group__DatasetItem.html": 14862648460163668817, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.pyi": 6616985736007157309, - "docs/api/html/group__LinkedDashboard.html": 16245670045855947237, - "docs/api/html/group__ComplianceStageRun.html": 4711615660021558641, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_argparse.py": 11111699418217357084, - "docs/api/html/group__frontend__src__lib__api__validation__js____duplicateTask.html": 9243462936463003663, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/ec_key.py": 5961540064444749833, - "docs/api/html/group__SemanticFieldEntry.html": 12684503746911343519, - "venv/lib/python3.13/site-packages/cryptography/x509/oid.py": 13909847799728532602, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/events.py": 15099787454570172797, - "docs/api/html/group__Route__Group__Contracts.html": 11731434895920260883, - "backend/src/api/routes/assistant/_admin_routes.py": 253209289619444139, - "frontend/src/lib/components/reports/__tests__/report_detail.ux.test.js": 7841254655898186872, - "venv/lib/python3.13/site-packages/anyio/abc/_testing.py": 16484863782452886547, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/implicit.py": 5400555157488808948, - "venv/lib/python3.13/site-packages/passlib/apache.py": 2357244937629445006, - "docs/api/html/group__update__storage__settings.html": 5443519149117079992, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/profiling.py": 16030397164203895281, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-2.csv": 7063857529719738394, - "venv/lib/python3.13/site-packages/rapidfuzz/process.pyi": 6959881293311340830, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/RECORD": 17985800801793506250, - "venv/lib/python3.13/site-packages/pygments/lexers/maple.py": 7402467573829618534, - "docs/api/html/group____build__documentation__export.html": 16699691799165712675, - "backend/src/core/plugin_base.py": 5063738955101125652, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/licenses/LICENSE": 5757922580822401219, - "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.pyi": 8916922961301555974, - "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.cpython-313-x86_64-linux-gnu.so": 5284769758475394095, - "docs/api/html/group____collect__src__files.html": 6109138608748972582, - "venv/lib/python3.13/site-packages/attrs/__init__.pyi": 14353885498883901855, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/incfile.f90": 4348588746493647387, - "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.py": 13446184230476677032, - "venv/lib/python3.13/site-packages/passlib/registry.py": 5050675058011850564, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py": 1551768441102117466, - "docs/api/html/group____parse__cron.html": 6292735699445558304, - "docs/api/html/group__cot__trace__context.html": 7845040058445266458, - "docs/api/html/group__get__auth__db.html": 7153540610662793603, - "venv/lib/python3.13/site-packages/numpy/_core/_internal.pyi": 6840742333427164920, - "venv/lib/python3.13/site-packages/fastapi/middleware/gzip.py": 436886407566263171, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/base.py": 10205051944814316388, - "venv/lib/python3.13/site-packages/pydantic/deprecated/tools.py": 16466182352694741629, - "frontend/src/lib/components/dataset-review/LaunchConfirmationPanel.svelte": 16168905765241471813, - "venv/lib/python3.13/site-packages/PIL/features.py": 3481835375516797487, - "docs/api/html/group__ExecutionMode.html": 359045115396835597, - "docs/api/html/globals_d.html": 2840545806846753350, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_multi.py": 4612816468258071810, - "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.pyi": 16176428602589420913, - "docs/api/html/group__MappingsApi.html": 11117168124577566737, - "docs/api/html/group__test__get__users__page__preserves__primary__schema__error__over__fallback__auth__error.html": 92546336677002310, - "docs/api/html/group__ConfigModels.html": 17708399333384858246, - "backend/src/api/routes/plugins.py": 6640856303389338188, - "docs/api/html/group__SupersetClientImportDashboard.html": 8056171795857604639, - "venv/lib/python3.13/site-packages/pandas/core/indexes/category.py": 18192569199634406001, - "docs/api/html/group__test__api__datasource__columns__missing__env.html": 12699662960552384887, - "docs/api/html/group__test__patch__dashboard__metadata__handles__missing__targets.html": 6681139266574011125, - "docs/api/html/group__handleSaveGlobal__Function.html": 5196092168626458120, - "docs/api/html/group__test__html__escaping.html": 6092029763861815279, - "venv/lib/python3.13/site-packages/charset_normalizer/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/asc.py": 10744437946030875496, - "frontend/src/lib/stores/maintenance.svelte.js": 17672315346665850067, - "docs/api/html/group__test__patch__dashboard__metadata__skips__when__no__metadata.html": 9093107496317548408, - "docs/api/html/group__record__clarification__answer.html": 18379043236673928126, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/METADATA": 3188324716322823755, - "venv/lib/python3.13/site-packages/pygments/lexers/mime.py": 7768464764849718825, - "backend/src/core/utils/network.py": 535557448085943960, - "docs/api/html/group__SupersetClientValidateImportFile.html": 9535308086571101211, - "venv/lib/python3.13/site-packages/pandas/tests/strings/__init__.py": 5060190599837877599, - "docs/api/html/group__TestTokenBudget.html": 11141209479743213441, - "docs/api/html/group__TranslationScheduler.html": 13712731753540269518, - "docs/api/html/group__extract__tables__from__jinja.html": 5053769374599061373, - "docs/api/html/group__ConfirmationRecord.html": 3457268499822373186, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.py": 183817263146423033, - "docs/api/html/group__test__add__logs__batch.html": 7044990428068455797, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_stack.py": 5316826578695144945, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayprint.py": 5912583751693302370, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_indexing.py": 2274965639578580723, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_sparse_accessor.py": 6774374030220202353, - "venv/lib/python3.13/site-packages/pandas/tests/strings/conftest.py": 12065119814157197510, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mutable.py": 9323088483998280629, - "venv/lib/python3.13/site-packages/httpcore/_sync/socks_proxy.py": 4418971149260853240, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_copy.py": 11335108864615756235, - "backend/src/models/task.py": 13936135007931808732, - "docs/api/html/group__add__logs.html": 9269565121918575356, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/licenses/LICENSE": 17020744174398828059, - "frontend/tailwind.config.js": 13139381085024634801, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/__init__.py": 15130871412783076140, - "backend/src/api/routes/__tests__/test_git_api.py": 4133163122739824202, - "venv/lib/python3.13/site-packages/pandas/_libs/properties.pyi": 9268470215750269699, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/type_check.pyi": 1599910480066368464, - "docs/api/html/group__docker__frontend__entrypoint__select__nginx__config.html": 5783880392055327340, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arithmetic.py": 17043958604607035425, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_shape_base.py": 8837845103338426570, - "docs/api/html/group__ComplianceReport.html": 8461864359499297379, - "docs/api/html/group__taskDrawer__Store.html": 6664598723983358111, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray.pyi": 14369009813591437391, - "backend/src/plugins/backup.py": 1465877198420371496, - "frontend/src/routes/validation/history/+page.svelte": 17086781413900596714, - "docs/api/html/group__test__create__task__success.html": 17553466713847616318, - "venv/lib/python3.13/site-packages/charset_normalizer/version.py": 9948878594498587774, - "frontend/build/_app/immutable/chunks/CC_MCBkD.js": 11709343780956289454, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/__init__.py": 6758781485387665020, - "venv/lib/python3.13/site-packages/pandas/tests/test_take.py": 17628004130770326677, - "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.py": 7029935879540617057, - "docs/api/html/group__BulkReplaceModalTests.html": 16436012889032153667, - "venv/lib/python3.13/site-packages/pandas/api/executors/__init__.py": 16954693181142010028, - "docs/api/html/group__UnifiedReportsPage.html": 4964051768663825925, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_info.py": 10615285239043745712, - "venv/lib/python3.13/site-packages/greenlet/greenlet_internal.hpp": 13173115501223013762, - "docs/api/html/group__DatasetReviewEnums.html": 8736640618120318797, - "docs/api/html/group__MockEnvPublic.html": 16155621137935179699, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_missing.py": 3991199131191840903, - "venv/lib/python3.13/site-packages/sqlalchemy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/multiarray.pyi": 6368268452184570195, - "docs/api/html/group__test__get__dashboards__override__show__all__contract.html": 16206749140891179616, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/decoder.py": 12653470212935324322, - "docs/api/html/group__engine.html": 6333404802173760004, - "docs/api/html/group__set__schedule.html": 8284012278188829172, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_version.py": 10384171076976071398, - "venv/lib/python3.13/site-packages/pandas/testing.py": 18352975429944961593, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_ndarray_backed.py": 10097812802469682233, - "docs/api/html/group__SupersetClientHeaders.html": 659607831527775901, - "docs/api/html/group__execute.html": 813912290626657753, - "venv/lib/python3.13/site-packages/rapidfuzz/process_py.py": 17563673072076876125, - "venv/lib/python3.13/site-packages/pip/_internal/utils/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pluggy/_tracing.py": 7276210024011809655, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x25519.py": 18229054440703471493, - "venv/lib/python3.13/site-packages/pygments/styles/emacs.py": 4540417536507245024, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/__init__.py": 10072943986583771248, - "docs/api/html/group__delete__role.html": 8546807030559673692, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api2.pyx": 15936470837420356071, - "docs/api/html/globals_func_f.html": 11627460573470994982, - "venv/lib/python3.13/site-packages/authlib/oidc/core/__init__.py": 16029653645350087968, - "docs/api/html/group__SupersetContextExtractorBase____extract__chart__id__from__state.html": 14330234105370642214, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/negative_bounds/issue_20853.f90": 3911644649277659333, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/inference.py": 2786441797589123900, - "venv/lib/python3.13/site-packages/jaraco/functools/__init__.pyi": 2872039192528394677, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reset_index.py": 14490875921264161445, - "venv/lib/python3.13/site-packages/websockets/version.py": 5853118844880112885, - "docs/api/html/search/functions_6.js": 16111349203442418291, - "docs/api/html/group__SessionPhase.html": 10683758452248873915, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssltransport.py": 8148652555908329980, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.cpython-313-x86_64-linux-gnu.so": 13209491675968970365, - "docs/api/html/group__detect__jinja__spans.html": 15712299430254929498, - "docs/api/html/group__handlePasswordPrompt__Function.html": 13969204145161370143, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_astype.py": 1961831758405941258, - "venv/lib/python3.13/site-packages/pandas/core/groupby/generic.py": 4857114071347950833, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.pyi": 9282436354926617898, - "frontend/src/lib/components/reports/__tests__/fixtures/reports.fixtures.js": 9590309593907049070, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_shift.py": 14026124784027171225, - "venv/lib/python3.13/site-packages/psycopg2/extensions.py": 14197445323992851566, - "docs/api/html/group__SmokeE2E.html": 5059040919589105636, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed25519.py": 2285778903282667561, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_na_indexing.py": 8712547126522523899, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/lexers/amdgpu.py": 17249439604111838979, - "frontend/src/components/RepositoryDashboardGrid.svelte": 8427306073875543545, - "venv/lib/python3.13/site-packages/websockets/py.typed": 15130871412783076140, - "docs/api/html/group__clean__release__enums.html": 3886475013764944523, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_factorize.py": 5585211699185937865, - "docs/api/html/group__test__sync__environment__skips__resources__without__uuid.html": 6431696107355961195, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_longdouble.py": 9337218715436213122, - "venv/lib/python3.13/site-packages/jsonschema/_legacy_keywords.py": 16481666133229715822, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/__init__.py": 1343779312297539596, - "venv/lib/python3.13/site-packages/pip/_internal/models/__init__.py": 13482731985065116750, - "venv/lib/python3.13/site-packages/numpy/core/fromnumeric.py": 2136433423902202138, - "frontend/src/routes/settings/FeaturesSettings.svelte": 14192374301379217586, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arraymethod.py": 14421188237635557797, - "venv/lib/python3.13/site-packages/passlib/handlers/fshp.py": 13298827601291507295, - "venv/lib/python3.13/site-packages/pygments/lexers/r.py": 14263133575134645005, - "venv/lib/python3.13/site-packages/pip/_internal/network/download.py": 5256731341219268693, - "venv/lib/python3.13/site-packages/packaging/_structures.py": 13086687542872305890, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop.py": 17940792593116540139, - "docs/api/html/group__ValidationPolicyUpdate.html": 14465919311238997171, - "venv/lib/python3.13/site-packages/_pytest/scope.py": 7603155109542903644, - "docs/api/html/group__TestConfigManagerCompat.html": 13874940420207193288, - "venv/lib/python3.13/site-packages/ecdsa/test_sha3.py": 12459459298663975730, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connectionpool.py": 10453724680327835821, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/rebuild.py": 1813298167468704155, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_setops.py": 14246225281428284850, - "venv/lib/python3.13/site-packages/pandas/_libs/hashing.pyi": 5241441378073929968, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_union_categoricals.py": 6843679254735807, - "venv/lib/python3.13/site-packages/anyio/py.typed": 15130871412783076140, - "docs/api/html/group__AuthOauthModule.html": 13530771519498131347, - "docs/api/html/group__import__dictionary__entries.html": 15238522570085331527, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayscalars.h": 16142050006935459679, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/groupby.py": 14661033005768429774, - "docs/api/html/group__get__chart__async.html": 1917535031433692607, - "venv/lib/python3.13/site-packages/packaging/_tokenizer.py": 7442392076059866359, - "docs/api/html/group__test__release__candidate__empty__id.html": 12372974039816984488, - "venv/lib/python3.13/site-packages/jeepney/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__test__get__dashboard__thumbnail__202.html": 9047480873947971115, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_reductions.py": 11624188672428941494, - "docs/api/html/group__test__deprecation__comment__exists.html": 5453492904479567908, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_constructors.py": 17392287676196409733, - "backend/tests/core/test_git_service_gitea_pr.py": 18003501172717000629, - "venv/lib/python3.13/site-packages/pygments/lexers/_ada_builtins.py": 16331776886501464264, - "frontend/src/routes/datasets/review/review-workspace-helpers.js": 8577563379877694502, - "scripts/gen_semantics.py": 11397714264377363083, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_py.py": 6518425342376455821, - "docs/api/html/group__verify__password.html": 16868760447655092510, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____resetSession.html": 1485006596273058906, - "docs/api/html/group__test__legacy__checks__endpoints__still__available.html": 6527479084678491892, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_hist_method.py": 2109257663592928, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/__init__.py": 15130871412783076140, - "docs/api/html/group__test__get__dashboards__success.html": 1205024512730425920, - "docs/api/html/group__LlmModels.html": 338390014476636, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/pretty.py": 2448541115957861214, - "docs/api/html/group__ResourceServiceModule.html": 7820716333589136956, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/wheel.py": 828222664427570058, - "docs/api/html/group__MigrationPackage.html": 671195743151997854, - "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe.py": 16525846817331842080, - "docs/api/html/group__selectPlugin__Function.html": 7924969411262828408, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/__init__.py": 15130871412783076140, - "backend/src/services/notifications/__tests__/test_notification_service.py": 13027609991239837872, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_indexing.py": 3841182456456779012, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.pyi": 1521384724035973688, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reflection.py": 1799734694597531652, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawer.html": 1057387600319463508, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/applicator": 6616723520993329538, - "docs/api/html/group____other__admin__user.html": 12434910978826868451, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/core.py": 2199607970842512780, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/_gen_files.py": 6273314671315377131, - "venv/lib/python3.13/site-packages/fastapi/openapi/models.py": 1680354178147364813, - "docs/api/html/group____load__config.html": 9007854862967223337, - "docs/api/html/group__get__environment.html": 1343499401732476775, - "docs/api/html/group__test__other__defaults__unchanged.html": 8009088910763350176, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi": 1065808244870331598, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/accesstype.f90": 877982462186396830, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/request.py": 10484280068366411541, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_diff.py": 7420674239386436885, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/version.py": 6401479036177132374, - "docs/api/html/group__warning.html": 8389706144157265523, - "backend/git_repos/remote/test-repo.git/hooks/push-to-checkout.sample": 4825338927654285910, - "docs/api/html/group__test__api__get__job__not__found.html": 11032532759238415335, - "docs/api/html/group__PolicyEngine.html": 10697573766276435812, - "docs/api/html/group__CleanReleaseTuiScript.html": 1449983814226815844, - "venv/lib/python3.13/site-packages/pandas/errors/__init__.py": 13444371880185740908, - "docs/api/html/group__AssistantFirstMessageIntegrationTest__Module.html": 16939727239361198220, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_convert.py": 17957207974799052653, - "venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py": 5076219228736706283, - "docs/api/html/group__LLMAnalysisModels.html": 10454605905980143274, - "venv/lib/python3.13/site-packages/_pytest/deprecated.py": 5452408732344157294, - "venv/lib/python3.13/site-packages/jeepney/tests/test_fds.py": 6257376331171761928, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77comments.f": 4801926170672147659, - "venv/lib/python3.13/site-packages/PIL/BlpImagePlugin.py": 7209218638257731375, - "frontend/build/_app/immutable/nodes/33.IQSVPmFm.js": 17762634792066116270, - "docs/api/html/group__test__provider__config.html": 10682630246046688970, - "docs/api/html/group__await__input.html": 16300840224339823745, - "docs/api/html/group__CreateAdminScript.html": 9770308413681571026, - "docs/api/html/group__handleCreate__Function.html": 16671349204777339326, - "docs/api/html/group____resolve__dashboard__title.html": 7648595970899356173, - "merge_kilo.py": 4282227344165325430, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_transform.py": 18159263102656491974, - "docs/api/html/group__list__permissions.html": 2593836930320988861, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_drop_duplicates.py": 12601598844977346213, - "venv/lib/python3.13/site-packages/PIL/ImageCms.py": 5610897002957128546, - "venv/lib/python3.13/site-packages/httpcore/_exceptions.py": 6716590711697088732, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.py": 5221550291012182443, - "backend/git_repos/remote/test-repo.git/refs/heads/master": 12485529318667335076, - "venv/lib/python3.13/site-packages/urllib3/http2/__init__.py": 62108352209996836, - "docs/api/html/group__resetForm__Function.html": 7796618286277147047, - "docs/api/html/group__getTaskStatus__Function.html": 16832723243113851896, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_typing.py": 17589387690638097975, - "backend/src/api/routes/storage.py": 3921659652525719249, - "venv/lib/python3.13/site-packages/smmap/test/lib.py": 2924931247676024085, - "venv/lib/python3.13/site-packages/pip/_internal/network/utils.py": 13421242949994555055, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/validation": 13992741783834278677, - "docs/api/html/group__test__virtual__dataset__match.html": 2595415352137646256, - "venv/lib/python3.13/site-packages/pygments/formatter.py": 11156733836937309465, - "docs/api/html/group__test__get__session__detail__export__and__lifecycle__endpoints.html": 7302188143370256832, - "venv/lib/python3.13/site-packages/httpx/_status_codes.py": 1758440600462491839, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_codes.py": 13901854719435716195, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/RECORD": 14375680579341422646, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildAssistantContextPrompt.html": 7622914159072277055, - "docs/api/html/group__frontend__src__services__adminService__js____updateUser.html": 10078748986285317841, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/RECORD": 11976403473353722969, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937.py": 7299586490442539723, - "docs/api/html/group__MaintenanceRoutesPackage.html": 8654226804027186583, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_symbol.py": 10792387279408528886, - "venv/lib/python3.13/site-packages/more_itertools/more.py": 1631135582441184133, - "venv/lib/python3.13/site-packages/pandas/tests/computation/test_eval.py": 2921337784087808487, - "venv/lib/python3.13/site-packages/fastapi/templating.py": 10760135144345238340, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/conftest.py": 2643915042664055969, - "venv/lib/python3.13/site-packages/PIL/ImageTk.py": 17063969714816564357, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/METADATA": 1266134223155266488, - "backend/src/services/rbac_permission_catalog.py": 11280653389166352517, - "docs/api/html/group__TranslateHelpersModule.html": 2380085236737921146, - "docs/api/html/group____append__history.html": 2245659271654816283, - "docs/api/html/group__test__patch__profile__preferences__success.html": 3112680572132768752, - "docs/api/html/group__test__candidate__registration__contract.html": 1989144001943621829, - "docs/api/html/group__goToPage__Function.html": 17867667754784109519, - "venv/lib/python3.13/site-packages/click/types.py": 6491651131669287534, - "venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py": 9639351759822608724, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_select.py": 7083040959665006634, - "docs/api/html/group__TargetSchemaColumnInfo.html": 8163902877022443310, - "docs/api/html/group__revoke__publication__endpoint.html": 828753596319821516, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/WHEEL": 7414522633964952515, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/unrolled.py": 12651425412640834530, - "venv/lib/python3.13/site-packages/gitdb/test/test_base.py": 16305779366785148795, - "venv/lib/python3.13/site-packages/pygments/plugin.py": 18006138132596504101, - "docs/api/html/group__SupersetCompilationAdapter__create__sql__lab__session.html": 13996430015600704960, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_index.py": 13517883160584591966, - "venv/lib/python3.13/site-packages/dateutil/rrule.py": 398469817784230512, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/RECORD": 11604408378998955169, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arithmetic.py": 3209727509456212551, - "frontend/src/routes/dashboards/__tests__/dashboard-profile-override.integration.test.js": 4930404579577741926, - "docs/api/html/group__test__list__runs__includes__language__info.html": 6499374490278250016, - "venv/lib/python3.13/site-packages/sqlalchemy/event/registry.py": 2857989070743633709, - "backend/src/api/routes/settings.py": 7176893095762662218, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/licenses/LICENSE": 9235234428907927646, - "docs/api/html/group__MigrationSettings.html": 7191961132337035743, - "backend/src/core/superset_client/_charts.py": 15609724925814918066, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas_constants.py": 14492427939536692313, - "docs/api/html/group____clear__assistant__state.html": 17985750078223833333, - "venv/lib/python3.13/site-packages/anyio/_core/_synchronization.py": 7050001375718876608, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.f90": 1249061697041863692, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_custom_dtypes.py": 12322100282959605055, - "docs/api/html/group__SupersetAccountLookupAdapter.html": 16273317223969819045, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_sort_values.py": 15072305514178699398, - "docs/api/html/group__getRepositoryStatusToken__Function.html": 49346559759034535, - "docs/api/html/group__LogEntryRow.html": 18132223528335475191, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/char.pyi": 1639810799219814643, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_strings.py": 7379299916213794543, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/METADATA": 14126682758562488244, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_numpy.py": 4073641912755651877, - "venv/lib/python3.13/site-packages/PIL/_imaging.pyi": 17119084207765761921, - "frontend/src/lib/ui/LanguageSwitcher.svelte": 15175441556916164874, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/_pytest/_py/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/csound.py": 2175454840368374515, - "docs/api/html/group__ApiKeysTab.html": 8274883643642903708, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs12.py": 5946672289916692272, - "venv/lib/python3.13/site-packages/packaging/pylock.py": 15346942039643310253, - "venv/lib/python3.13/site-packages/itsdangerous/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_py_util.py": 12836989066113400538, - "docs/api/html/group__test__maintenance__service.html": 4028053446391236615, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/crud.py": 805684028038896506, - "venv/lib/python3.13/site-packages/anyio/streams/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_win32_console.py": 18092547253031555768, - "venv/lib/python3.13/site-packages/pydantic/typing.py": 6661252174518316760, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/array.py": 13840510238256626632, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py": 2516476609539242190, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reindex.py": 12459492712890114705, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_online.py": 11850497659869773510, - "backend/src/schemas/auth.py": 1879729190428074314, - "venv/lib/python3.13/site-packages/pygments/lexers/haxe.py": 1915270891525929751, - "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.py": 17192211156810267818, - "docs/api/html/group__test__get__dashboards__superset__failure.html": 4480500139441012004, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_object.py": 9215781825358309147, - "docs/api/html/group__test__enterprise__policy__external__allowed.html": 13390827241810832379, - "docs/api/html/group__SourceRegistrySnapshot.html": 16441767339963706656, - "docs/api/html/group__MaintenanceAlreadyActiveResponse.html": 3971170803741620113, - "docs/api/html/group__ProfileSecuritySummary.html": 13465338754935140981, - "docs/api/html/group__PolicyForm.html": 18395519007487395459, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets2.py": 11100501798741918610, - "docs/api/html/group__test__resolve__trusted__policy__snapshots__rejects__override__attempt.html": 17876948714761629910, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/index_tricks.pyi": 11463747060334597941, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_categorical.py": 12897027063618569795, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/attr/validators.pyi": 15579625896285277321, - "venv/lib/python3.13/site-packages/pygments/lexers/css.py": 517586374264113606, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_analytics.py": 8155550493311083506, - "backend/src/core/auth/security.py": 11836436636390315707, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_mask.py": 5451866071307236899, - "docs/api/html/group__test__create__admin__creates__user__with__optional__email.html": 4774635466855603335, - "docs/api/html/group__FileCategory.html": 1512310133619337976, - "docs/api/html/group__create__validation__policy.html": 17844050931400995860, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/WHEEL": 14929202952940710322, - "venv/lib/python3.13/site-packages/authlib/oidc/core/userinfo.py": 17201158389976619872, - "venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py": 3095012838116670328, - "venv/lib/python3.13/site-packages/pandas/core/arrays/__init__.py": 10460719978124302175, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_pytables_missing.py": 12154582133989896069, - "venv/lib/python3.13/site-packages/pandas/io/iceberg.py": 13137128885014736233, - "venv/lib/python3.13/site-packages/keyring/backends/macOS/api.py": 4502902558534812719, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_linux.h": 4071314837793906998, - "frontend/src/routes/dashboards/[id]/components/DashboardGitManager.svelte": 12995972702842177800, - "frontend/build/_app/immutable/chunks/Cn56Obuw.js": 9774722709007984298, - "venv/lib/python3.13/site-packages/dotenv/parser.py": 17651523026132242005, - "frontend/build/_app/immutable/nodes/23.D5IfY_Wm.js": 4234056891832193499, - "frontend/src/components/tasks/TaskResultPanel.svelte": 2978429516796744791, - "docs/api/html/group__get__providers.html": 2676511877365075835, - "docs/api/html/group____valid__artifacts.html": 18020364086775197515, - "venv/lib/python3.13/site-packages/PIL/ExifTags.py": 6869879826623916830, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_unary.py": 2573825678342936734, - "docs/api/html/group__TranslationBatch.html": 8584550832559177298, - "frontend/src/routes/datasets/DatasetPreview.svelte": 4250995999947659034, - "docs/api/html/group__test__tui__exit__f10.html": 6092888054021050903, - "docs/api/html/group__test__cors__parsing__with__trailing__spaces.html": 2388414963323398944, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_common.h": 14732581790650403546, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dropna.py": 3168969662216766293, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/module_data_docstring.f90": 18245403117623015312, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/WHEEL": 3749476255729626245, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_produces_warning.py": 750781117570375777, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_put.py": 8547660958963625817, - "docs/api/html/group__ManifestSummary.html": 5053023856336130679, - "venv/lib/python3.13/site-packages/numpy/tests/test_scripts.py": 5790352482583964348, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/umath/svml/LICENSE": 9584102918819171104, - "docs/api/html/group__test__continue__merge__passes__message__and__returns__commit.html": 7335459609919818162, - "docs/api/html/group__AssistantApi__Module.html": 4671602144528618803, - "venv/lib/python3.13/site-packages/pydantic/schema.py": 83238111330132033, - "docs/api/html/group__MapperPluginModule.html": 12912874537129750677, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_pad.pyi": 14755435694146003896, - "venv/lib/python3.13/site-packages/git/repo/fun.py": 9019364012746153312, - "venv/lib/python3.13/site-packages/pandas/io/clipboards.py": 5183212433608887727, - "docs/api/html/group__create__sub__context.html": 6446207849507974736, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/accumulate.py": 5780225603819849877, - "docs/api/html/group__register__candidate.html": 9069772319908727023, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadb.py": 3109042476851791996, - "venv/lib/python3.13/site-packages/websockets/sync/client.py": 10892557975919033983, - "docs/api/html/group__TranslateApiTests__Class.html": 5018753849981248243, - "venv/lib/python3.13/site-packages/pyasn1/type/__init__.py": 15728752901274520502, - "docs/api/html/group__AsyncAPIClient__get__headers.html": 4827871173278773347, - "venv/lib/python3.13/site-packages/_pytest/_version.py": 4890545974654141536, - "frontend/src/components/Toast.svelte": 1736760619841085958, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/forth.py": 17623561228839319663, - "docs/api/html/group__TestDictionaryPromptBuilder.html": 990574254125574660, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_cell_widths.py": 5724379364993806351, - "venv/lib/python3.13/site-packages/annotated_doc/main.py": 3384049281532298500, - "docs/api/html/group__get__api__key__principal.html": 5141844013262669360, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/__init__.py": 15130871412783076140, - "docs/api/html/group__superset__lookup__service.html": 13295169492630932341, - "docs/api/html/group__frontend__src__lib__api__validation__js____updateTask.html": 5295403112831889335, - "backend/src/api/routes/translate/_job_routes.py": 10128664562563592050, - "docs/api/html/group____get__owned__mapping__or__404.html": 17632372297671515109, - "docs/api/html/group__test__get__task__logs__success.html": 14138133046940925663, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape.pyi": 2188930129299348535, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_cut.py": 7800833253841657380, - "venv/lib/python3.13/site-packages/passlib/__init__.py": 6987142392711441525, - "docs/api/html/group__configure__logger.html": 6165988274497149791, - "venv/lib/python3.13/site-packages/pygments/styles/__init__.py": 18383248976818885498, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_ratio.py": 940213488194452623, - "frontend/src/lib/ui/index.ts": 6883965364006817450, - "docs/api/html/group____run__async.html": 1312288416173170323, - "docs/api/html/search/groups_12.js": 10902160763074406551, - "docs/api/html/group__resumeTask__Function.html": 15320819225352081425, - "dist/docker/backend.0.1.1.tar.xz": 932529191402352855, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/authorization_server.py": 2913800420669417152, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_install.py": 7181005402818830655, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/socks.py": 17581293673028078134, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/WHEEL": 12054782055241301457, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE": 12064892310002036996, - "frontend/playwright-report/trace/assets/defaultSettingsView-D31xz8zv.js": 1301407320635785789, - "docs/api/html/globals_v.html": 10195887458242636103, - "docs/api/html/group__run__mapping.html": 4039233635360893448, - "venv/lib/python3.13/site-packages/pandas/core/arrays/integer.py": 7775055011080964709, - "backend/src/services/__tests__/test_llm_plugin_persistence.py": 17921598588767566887, - "venv/lib/python3.13/site-packages/pygments/styles/native.py": 4559566444883341527, - "docs/api/html/group__get__preferences.html": 14948180484738743961, - "venv/lib/python3.13/site-packages/rapidfuzz/process.py": 3870006735299537016, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/rethinkdb.py": 16413299263077970925, - "venv/lib/python3.13/site-packages/numpy/random/_pcg64.pyi": 14785496840737986355, - "docs/api/html/group__should__log__task__level.html": 3031004604954435079, - "frontend/src/services/__tests__/gitService.test.js": 2011135397906862721, - "venv/lib/python3.13/site-packages/pandas/io/formats/style.py": 13235403689320479165, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py": 12632271159842988561, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____stringifyValue.html": 757351777204500206, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_sequence.py": 3721630758772772345, - "docs/api/html/group____check__translation__cache.html": 3321256720514303969, - "venv/lib/python3.13/site-packages/yaml/constructor.py": 8731384179016992045, - "docs/api/html/group__ToolsService.html": 2114074285056843811, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/METADATA": 6438256145634479451, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_crackfortran.py": 10278720705326601943, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_equals.py": 11041884432990762650, - "docs/api/html/group__test__persist__task__update.html": 8785260065895714433, - "docs/api/html/group__get__config__manager.html": 9948904039819241913, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/index.py": 15694995431926521471, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jws_algs.py": 11920621906391796226, - "venv/lib/python3.13/site-packages/httpx/_auth.py": 1283189552342958900, - "venv/lib/python3.13/site-packages/authlib/oauth2/base.py": 12948318231066662045, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/columns.py": 8488246354818721406, - "docs/api/html/group__Toast.html": 15408687886829508162, - "docs/api/html/group____resolve__dashboard__id__entity.html": 6374255965303558769, - "docs/api/html/group__get__type__profile.html": 8801753117531383071, - "venv/lib/python3.13/site-packages/fastapi/background.py": 16533963020573865222, - "venv/lib/python3.13/site-packages/pydantic/deprecated/__init__.py": 15130871412783076140, - "docs/api/html/group__mock__api__deps.html": 6995100017269636667, - "docker/frontend.entrypoint.sh": 11397342350356400663, - "venv/lib/python3.13/site-packages/pandas/core/arrays/interval.py": 16281221279220043867, - "docs/api/html/group__TestAssistantAuthz.html": 3742284651131782250, - "docs/api/html/group__ReportsListTest__Module.html": 16813306558696724690, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/METADATA": 1862765380818895200, - "docs/api/html/group__test__logger.html": 11504400505756254792, - "venv/lib/python3.13/site-packages/_pytest/setupplan.py": 7496801885995567141, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/horizontal_shard.py": 17615732108308858890, - "docs/api/html/group____terminal__run.html": 17656532889079902778, - "docs/api/html/group__get__clean__release__repository.html": 3802772279404902522, - "frontend/src/components/TaskLogViewer.svelte": 15158021998452088463, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/core": 6622984507059115517, - "docs/api/html/group____apply__dashboard__filters.html": 13208356314740961922, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval.py": 18040371224712437126, - "backend/tests/services/dataset_review/test_superset_matrix.py": 3788494235626090289, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_pydatetime.py": 16228315407662560992, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_clipboard.py": 12674078499544938252, - "venv/lib/python3.13/site-packages/passlib/tests/test_win32.py": 13932956220195912963, - "docs/api/html/group____seed__repository.html": 7316844014942440184, - "backend/src/core/utils/superset_context_extractor/_parsing.py": 18406152325228380468, - "docs/api/html/group____apply__features__from__env.html": 5044527824719200918, - "docs/api/html/navtreeindex14.js": 14969246354997611461, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.pyi": 2700568594438441246, - "venv/bin/uvicorn": 3776670267111625708, - "docs/api/html/group____save__config__to__db.html": 11046536065710831313, - "docs/api/html/group__MigrationJob.html": 8882884563951880280, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/_mapping.py": 17519715342700184302, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_size.py": 8527839563560550806, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/comparisons.py": 7327629888601690759, - "frontend/src/lib/components/reports/ReportDetailPanel.svelte": 5659980083465412500, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/region.py": 12579403230062395563, - "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.pyi": 9243829615414967057, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo90.f90": 2435648607844815947, - "docs/api/html/group__MaintenanceBannerPage.html": 9819977167182180539, - "venv/lib/python3.13/site-packages/pluggy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_cipheralgorithm.py": 10057637461647348368, - "venv/lib/python3.13/site-packages/passlib/apps.py": 4633269405597895766, - "venv/lib/python3.13/site-packages/dateutil/tz/_common.py": 9009072600471223066, - "venv/lib/python3.13/site-packages/uvicorn/loops/asyncio.py": 16496916527760969173, - "docs/api/html/group__test__plugin__loader__logger__calls__exist.html": 14504869611475287296, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/RECORD": 15865206372525596795, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_s3.py": 17425535047477778484, - "backend/src/services/dataset_review/semantic_resolver.py": 11369306880421964134, - "docs/api/html/group__test__revoke__unknown__publication__rejected.html": 2648436309555498591, - "docs/api/html/group____is__dashboard__endpoint.html": 16400823815894598847, - "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.py": 548928042543500365, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/keywrap.py": 10424037418528425800, - "backend/src/plugins/translate/_batch_proc.py": 5830558923416766212, - "backend/tests/test_translate_scheduler_execution.py": 16687335504320865378, - "docs/api/html/group__MetricsTable.html": 17283314771986202846, - "venv/lib/python3.13/site-packages/jaraco/functools/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arithmetic.pyi": 467927803068643024, - "venv/lib/python3.13/site-packages/numpy/fft/_helper.py": 15906444100774706395, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/algorithms.py": 2375452080667815941, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/licenses/LICENSE": 3509878235770224233, - "docs/api/html/group__test__get__job.html": 17537338432908316746, - "docs/api/html/group__AdminService.html": 15757284534026835957, - "docs/api/html/group__test__prepare__candidate__api__success.html": 2373678927354742506, - "docs/api/html/group__MigrationPlugin.html": 12305721336063597680, - "docs/api/html/group__PreviewEnqueueResultResponse.html": 6184757346716551211, - "docs/api/html/globals_l.html": 8615069986131999743, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dropna.py": 17272771246760486947, - "docs/api/html/group__GitConfigRoutes.html": 1739917609920142885, - "venv/lib/python3.13/site-packages/pip/_internal/utils/filesystem.py": 7716285033598020316, - "venv/lib/python3.13/site-packages/pygments/lexers/xorg.py": 14591144191879110061, - "docs/api/html/group__apply__record__action.html": 5054356330772600177, - "venv/lib/python3.13/site-packages/PIL/ImageColor.py": 17058618058554970738, - "venv/lib/python3.13/site-packages/pygments/styles/lightbulb.py": 12433916113773598760, - "docs/api/html/group__AssistantDispatch.html": 16287296406438246956, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_smoke.py": 7476432112986643847, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/default_styles.py": 4469204219504675080, - "venv/lib/python3.13/site-packages/starlette/_exception_handler.py": 9892172656847200124, - "venv/lib/python3.13/site-packages/pandas/io/excel/_openpyxl.py": 6676451564208867775, - "docs/api/html/globals_func_m.html": 5387554071243242205, - "venv/lib/python3.13/site-packages/itsdangerous/timed.py": 12571102606719555007, - "venv/lib/python3.13/site-packages/sqlalchemy/util/typing.py": 17638587764345605306, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py": 10367325397209016951, - "venv/lib/python3.13/site-packages/pygments/lexers/x10.py": 6167378515780115912, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/METADATA": 9041650844081036822, - "docs/api/html/group__fetchData__Function.html": 12479170828711469156, - "docs/api/html/group__getReports__Function.html": 10989136831349411204, - "docs/api/html/group__test__permission__denied__all__actions.html": 9271173936930380116, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/WHEEL": 5838667082726674838, - "frontend/src/routes/dashboards/[id]/components/DashboardLinkedResources.svelte": 2349700459673295788, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/__init__.py": 15130871412783076140, - "docs/api/html/group__UserUpdate.html": 17001565136687815956, - "docs/api/html/group____resolve__candidate__and__report.html": 16524530636169236472, - "venv/lib/python3.13/site-packages/numpy/ma/extras.py": 7559838309481824664, - "docs/api/html/group__TaskDrawerStoreTest__Module.html": 12333000150269624051, - "backend/src/services/clean_release/stages/base.py": 14966559035592717091, - "docs/api/html/group__test__build__dataset__preview__query__context__preserves__time__range__from__native__filter__payload.html": 814719278183998378, - "venv/lib/python3.13/site-packages/git/index/fun.py": 18338379518768926372, - "frontend/src/components/TaskHistory.svelte": 4575283937108155085, - "docs/api/html/group__abort__merge.html": 8636553700350043115, - "docs/api/html/search/functions_14.js": 8813812505202203027, - "venv/lib/python3.13/site-packages/h11/_version.py": 9334144031400715479, - "docs/api/html/group____filters__imports.html": 16499647191503680899, - "docs/api/html/group__test__no__schema__qualified__tables.html": 4881082392133398377, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_cumulative.py": 11752712818547022872, - "venv/lib/python3.13/site-packages/charset_normalizer/__main__.py": 8887772289570078278, - "docs/api/html/group__test__get__dashboards__profile__filter__no__match__results__contract.html": 6248644768287182615, - "backend/src/services/clean_release/dto.py": 16033619016556005561, - "docs/api/html/group__frontend__src__lib__utils__js____cn.html": 8947971455954097415, - "docs/api/html/group__load__Function.html": 10190139410181623652, - "docs/api/html/group__test__jinja__string__concat.html": 16396089337050427793, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/__init__.py": 913610541217778747, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/METADATA": 16700674024421191168, - "venv/lib/python3.13/site-packages/numpy/random/_philox.cpython-313-x86_64-linux-gnu.so": 9916143171057774705, - "docs/api/html/group__LaunchDatasetResponse.html": 17085095085234548738, - "venv/lib/python3.13/site-packages/pandas/core/computation/expressions.py": 10684284531040203747, - "venv/lib/python3.13/site-packages/click/decorators.py": 2920877667855377423, - "docs/api/html/group__test__release__candidate__valid.html": 9747216871494350774, - "venv/lib/python3.13/site-packages/pip/_internal/cli/spinners.py": 7664056305019151182, - "docs/api/html/group__frontend__src__services__git__utils__js____applyGitflowStageDefaults.html": 10826927969679877530, - "backend/src/plugins/migration.py": 10457528901248546685, - "docs/api/html/group__ResourceMapping.html": 12067212261017894140, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/METADATA": 14070889071949105584, - "docs/api/html/group__ReportsApiTest.html": 634823893276138685, - "docs/api/html/group__addToast__Function.html": 13046327232086323563, - "docs/api/html/group__LoggingSettings.html": 1027847486912759228, - "docs/api/html/group__FileIO.html": 16081394467127736340, - "docs/api/html/group__run__approve.html": 18017929900631845017, - "venv/lib/python3.13/site-packages/pandas/tests/config/test_config.py": 4630144191929135452, - "frontend/src/lib/auth/permissions.js": 7186663483939824478, - "docs/api/html/group__test__get__sources.html": 11705664249388270176, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/zookeeper.py": 4318643109309626098, - "venv/lib/python3.13/site-packages/numpy/random/_sfc64.cpython-313-x86_64-linux-gnu.so": 17186695607612591922, - "docs/api/html/globals_func_c.html": 11616926192994037587, - "docs/api/html/group__ColumnDescriptionUpdate.html": 13860285606848382812, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_indexing.py": 16291207621268530224, - "docs/api/html/group__sanitize__text.html": 15662367841313346736, - "venv/lib/python3.13/site-packages/numpy/lib/user_array.pyi": 1968484998382771986, - "docs/api/html/group__SourceIntakePanel.html": 556586784232827301, - "docs/api/html/group__TranslateHistoryPage.html": 13167035080416259682, - "docs/api/html/group____build__banner__text__for__dashboard.html": 5689918064001964476, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro_py.py": 11991492232381887787, - "docs/api/html/group____make__requests__http__error.html": 13378194080112951253, - "venv/lib/python3.13/site-packages/httpx/_multipart.py": 2181282475656908730, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/scope.py": 4096190764836489091, - "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.py": 17055498803745289016, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/aioodbc.py": 12704729841432715246, - "docs/api/html/group__delete__task__with__logs.html": 14316927894050853121, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows_renderer.py": 1076842273781909680, - "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.ux.test.js": 3080911695504204499, - "docs/api/html/group__get__run__status.html": 4185917444778756301, - "docs/api/html/group__LaunchConfirmationPanel.html": 10912367734580070482, - "docs/api/html/group__test__schedule__not__found__skips.html": 10866822650203142876, - "docs/api/html/group____FakeQueryDel.html": 12251734858726214042, - "docs/api/html/group__SupersetContextExtractorBase____decode__query__state.html": 5669940893460409072, - "venv/lib/python3.13/site-packages/pandas/tests/resample/conftest.py": 1643522609234951699, - "docs/api/html/group__GitPluginModule.html": 9711439707382347339, - "venv/lib/python3.13/site-packages/pygments/styles/arduino.py": 4578385147314541233, - "docs/api/html/group__test__transform__yaml__nonexistent__file.html": 12815312108870320072, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA": 18410956221497090898, - "docs/api/html/group__remove__chart__from__layout.html": 9632071695913277477, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_api.py": 16743710460729009914, - "docs/api/html/group____flusher__loop.html": 18087124842264758700, - "venv/lib/python3.13/site-packages/_pytest/pytester_assertions.py": 1899505062406979663, - "docs/api/html/group__confirmAssistantOperation__Function.html": 12036132904812216461, - "frontend/tests/maintenance.test.ts": 18128393571681331195, - "venv/lib/python3.13/site-packages/greenlet/tests/test_generator_nested.py": 12822240347707346718, - "venv/lib/python3.13/site-packages/websockets/asyncio/connection.py": 17044409627448400723, - "docs/api/html/group__BackupsPage.html": 927182552608523101, - "venv/lib/python3.13/site-packages/jeepney/wrappers.py": 15234251727654692435, - "docs/api/html/group____process__states__for__end.html": 17185750365367165889, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/__init__.py": 7062045842767697645, - "docs/api/html/group__test__setActiveItem__Function.html": 8801230193725679964, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_dict_compat.py": 18196543090455290455, - "docs/api/html/group__SemanticCandidate.html": 16155484952491481356, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.cpython-313-x86_64-linux-gnu.so": 1137453907225619959, - "venv/lib/python3.13/site-packages/urllib3/_base_connection.py": 14495079097019980154, - "frontend/src/lib/api/reports.js": 12520710039739782171, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_period.py": 15671388508498441412, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data": 9599129816090399315, - "docs/api/html/group__Token.html": 5997049889797174101, - "venv/lib/python3.13/site-packages/fastapi/openapi/utils.py": 9058442333718187759, - "docs/api/html/group__DatasetReviewExecutionModels.html": 15585823821459317231, - "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so": 2888939866336522308, - "docs/api/html/group__user__roles.html": 5258273442326465678, - "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key__slug.html": 10954317903886069968, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_rewrite_warning.py": 4895919872954538758, - "venv/lib/python3.13/site-packages/pandas/core/computation/expr.py": 13085709365673019494, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/idna/intranges.py": 12536174834761591006, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/models.py": 17356033861792227817, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_frame.py": 4303613319747945709, - "docs/api/html/group__MaintenanceEventItem.html": 15520444473677444041, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/RECORD": 963865046004908868, - "venv/lib/python3.13/site-packages/anyio/_core/_eventloop.py": 6853955946028131785, - "venv/lib/python3.13/site-packages/websockets/speedups.pyi": 10322382420215791091, - "venv/lib/python3.13/site-packages/pillow.libs/libopenjp2-94e588ba.so.2.5.4": 2483101806045986252, - "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/npymath.ini": 1071371630695963467, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_base.py": 8678387511873412922, - "backend/src/api/routes/dashboards/_schemas.py": 4116381929361209080, - "docs/api/html/group__TestResourceHubs.html": 18340295788009698355, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_gcs.py": 7733247456252965901, - "examples/maintenance-api-python.py": 18279360934407602079, - "venv/lib/python3.13/site-packages/pip/__pip-runner__.py": 1138979712722199296, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_indexing.py": 16264430536704066847, - "docs/api/html/group__StoredFile.html": 17068629307196027308, - "docs/api/html/group__discover__declared__permissions.html": 14317626307828346282, - "docs/api/html/group__SupersetContextTemplatesMixin____append__template__variable.html": 14259073888034384633, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_accuracy.py": 3904008607472505376, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/setup.py": 5482352991573617756, - "docs/api/html/group__DictionaryCorrectionService.html": 5320595430025547036, - "docs/api/html/group__test__reload__validation__policy__adds__job.html": 1850773905928943851, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py": 11185011314498348980, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunc_config.py": 14728417910466146460, - "docs/api/html/group__LLMProviderService.html": 12709095326863843331, - "docs/api/html/group__test__lookup__superset__accounts__success.html": 21505406146454782, - "venv/lib/python3.13/site-packages/attr/_compat.py": 4016698632899969390, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/base.py": 10200916670889920659, - "docs/api/html/group__test__empty__content.html": 11172556345158624109, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/METADATA": 3643456577199395707, - "docs/api/html/group__resume__task.html": 10211397007728257393, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_scalar_compat.py": 1306236983392218709, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_calendar.py": 12313641252472031631, - "docs/api/html/group__frontend__src__services__git__utils__js____isNumericDashboardRef.html": 10032907620430901043, - "backend/src/core/cot_logger.py": 9539001363130629806, - "backend/src/plugins/translate/sql_generator.py": 14497259013250233663, - "venv/lib/python3.13/site-packages/pandas/tests/apply/common.py": 1113935558495455492, - "venv/lib/python3.13/site-packages/gitdb/fun.py": 55226330810782184, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_dists.py": 16746363359320577809, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/introspection.py": 5447179017910128771, - "backend/src/services/reports/__init__.py": 11818812865847046400, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_join.py": 17874591841487372992, - "backend/src/api/routes/clean_release.py": 9890311709059474429, - "docs/api/html/group__SupersetContextFiltersExtractMixin.html": 8312542412809507085, - "docs/api/html/group____make__preview__ready__session.html": 4654960038289930410, - "venv/lib/python3.13/site-packages/cffi/api.py": 5325821560978502135, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x448.pyi": 11733449165276720901, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_categorical.py": 10863578845754547294, - "frontend/playwright-report/data/e22baf04065a196fa914a4a2640b3e055cda5768.zip": 2929961739056234978, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isna.py": 18394053826166267096, - "backend/src/core/rate_limiter.py": 8058892433074032268, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_year.py": 1478857009124807836, - "docs/api/html/group__handleDeleteGiteaRepo__Function.html": 8343489638685254730, - "docs/api/html/group__debug.html": 5353036165559255799, - "docs/api/html/search/close.svg": 6737892306857409458, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py": 1131520422778420758, - "backend/src/services/clean_release/manifest_builder.py": 16367921257027024830, - "docs/api/html/group__TranslateRunListRoutesModule.html": 7983808577352812807, - "docs/api/html/group__DashboardHelpers.html": 17415235275942195386, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_methods.py": 14881910898871057761, - "docs/api/html/group__get__tasks.html": 6039993422624738994, - "docs/api/html/group__fetchPlugins__Function.html": 3672886601071817310, - "venv/lib/python3.13/site-packages/git/index/base.py": 641556304637379636, - "venv/lib/python3.13/site-packages/numpy/random/c_distributions.pxd": 16574161095720077595, - "frontend/src/routes/storage/+page.svelte": 5003283673039328108, - "venv/lib/python3.13/site-packages/pandas/_libs/reshape.cpython-313-x86_64-linux-gnu.so": 10639750501333255943, - "docs/api/html/group__MetricsResponse.html": 2931960474908647551, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/descriptor_props.py": 7044852927393650707, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_http_headers.py": 13187746870209705951, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/interface.py": 3134071530627935964, - "docs/api/html/group__SupersetDatabasesMixin.html": 296914084518346512, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/__init__.py": 15130871412783076140, - "frontend/build/_app/immutable/nodes/17.Dbd7wpxW.js": 11111564486711173973, - "docs/api/html/group____cleanup__history__ttl.html": 12209285080355144481, - "venv/lib/python3.13/site-packages/pandas/_libs/writers.pyi": 10494773026133244183, - "venv/lib/python3.13/site-packages/urllib3/_request_methods.py": 13671055114247516883, - "docs/api/html/group__AsyncAPIClient____is__dashboard__endpoint.html": 11542093610496385064, - "docs/api/html/navtreeindex3.js": 4980194464875678268, - "docs/api/html/tab_a.png": 119030213786483377, - "docs/api/html/group__test__sanitize__imported__filter__for__assistant__masks__sensitive__raw__values.html": 9414613568874782438, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.py": 17993169044119596860, - "venv/lib/python3.13/site-packages/pip/_internal/cli/__init__.py": 11914508471759255927, - "docs/api/html/group__GitReleasePanel.html": 2228780701983179318, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth1_session.py": 1911069558956893789, - "docs/api/html/group__FieldKind.html": 4665292646026475164, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authenticate_client.py": 16886645731942472285, - "venv/lib/python3.13/site-packages/numpy/_configtool.pyi": 9670892208858157218, - "venv/lib/python3.13/site-packages/pygments/styles/abap.py": 11543320440974964547, - "venv/lib/python3.13/site-packages/pandas/_testing/__init__.py": 10127496080404419481, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/base.py": 16052592982063844259, - "frontend/src/lib/components/reports/__tests__/reports_filter_performance.test.js": 6358696236009732713, - "backend/tests/test_api_key_auth.py": 15854247672009604904, - "docs/api/html/group____base__imports.html": 13349247487959032441, - "docs/api/html/group__now.html": 883313604471122224, - "docs/api/html/group__ComplianceStageContext.html": 15065570275790964378, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py": 5951113078754792806, - "docs/api/html/group__MaintenanceModels.html": 14224150703489813843, - "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.cpp": 7360124705400993707, - "docs/api/html/group__get__repository__binding.html": 642256703362197453, - "venv/lib/python3.13/site-packages/pandas/tests/frame/common.py": 7426635866622531875, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/fields.py": 10090313990429695520, - "venv/lib/python3.13/site-packages/uvicorn/middleware/message_logger.py": 14825516604555033580, - "frontend/src/routes/datasets/review/[id]/+page.svelte": 3692143812318259790, - "venv/lib/python3.13/site-packages/attrs/setters.py": 5775359015245600592, - "venv/lib/python3.13/site-packages/sqlalchemy/util/queue.py": 1289234691048379395, - "docs/api/html/group__test__duplicate__job__custom__name.html": 1186685615290746284, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__NavigationContracts.html": 585565417315368787, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE": 11986475601091748012, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_textreader.py": 388955620768371652, - "venv/lib/python3.13/site-packages/py.py": 16248211404003181569, - "venv/lib/python3.13/site-packages/pandas/api/indexers/__init__.py": 17431573013142626187, - "frontend/build/_app/immutable/chunks/DGNzAwKo.js": 17568648881596997070, - "docs/api/html/group__TestReportsOpenapiConformance.html": 14837764071303153325, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_at_time.py": 15352346149584081864, - "venv/lib/python3.13/site-packages/numpy/random/_sfc64.pyi": 3536657185495782642, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/privatemod.f90": 8999550841368314739, - "venv/lib/python3.13/site-packages/pandas/core/reshape/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/__init__.py": 14951958843029076911, - "docs/api/html/group__frontend__src__services__storageService__js____getStorageAuthHeaders.html": 18312324080831107841, - "venv/lib/python3.13/site-packages/urllib3/util/url.py": 15992369651965016538, - "venv/lib/python3.13/site-packages/jeepney/bus.py": 5163075760432139100, - "docs/api/html/group__test__get__logs__with__pagination.html": 2508602189967508929, - "docs/api/html/navtreeindex17.js": 16370759160904060398, - "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet.py": 10727595500186011102, - "venv/lib/python3.13/site-packages/sqlalchemy/future/engine.py": 4639404437703898767, - "frontend/build/_app/immutable/nodes/5.CvEvHdok.js": 7069431821432479784, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_bcrypt.py": 11146468670439284889, - "venv/lib/python3.13/site-packages/pandas/core/internals/ops.py": 4740509262379451243, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/operators.py": 2468046993343796128, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunc_config.pyi": 11754104063443791746, - "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.py": 17277594393032382485, - "docs/api/html/group____discover__plugin__execute__permissions__cached.html": 2910829467411233416, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_riscv_unix.h": 8607317131295864391, - "venv/lib/python3.13/site-packages/numpy/_core/_methods.py": 16805668769357938792, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_numba.py": 6409810497251251179, - "venv/lib/python3.13/site-packages/click/exceptions.py": 9734955259447933725, - "venv/lib/python3.13/site-packages/rapidfuzz/utils_cpp.cpython-313-x86_64-linux-gnu.so": 16145829319198889672, - "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py": 359905908083162759, - "docs/api/html/group__profile__service.html": 18177058371654387128, - "docs/api/html/group__TargetSchemaValidationRequest.html": 12191974137983183185, - "docs/api/html/group__docker__backend__entrypoint__install__playwright.html": 13661351030986705030, - "docs/api/html/group__TranslationPreviewResponse.html": 10623704899221380546, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_astype.py": 7497268667246913350, - "docs/api/html/group__TranslationRunResponse.html": 11155567943243725782, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_invalid_arg.py": 610891871484326055, - "docs/api/html/group__ProfileFixtures__Module.html": 11831895659314053968, - "docs/api/html/group__frontend__src__services__adminService__js____getLoggingConfig.html": 10395249450462043320, - "venv/lib/python3.13/site-packages/authlib/common/encoding.py": 52296066492753953, - "venv/lib/python3.13/site-packages/pandas/util/_doctools.py": 3766479022075611396, - "docs/api/html/group__getAssistantHistory__Function.html": 15491234740107286264, - "docs/api/html/search/all_d.js": 15064235828292081974, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py": 16463243654123731993, - "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.py": 15989335816452913951, - "docs/api/html/tab_sd.png": 7906432400932174880, - "venv/lib/python3.13/site-packages/pandas/_libs/parsers.pyi": 18402185996588014111, - "docs/api/html/group__test__throttled__scheduler.html": 9291093931746926808, - "venv/lib/python3.13/site-packages/numpy/strings/__init__.py": 6523674574341354095, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_conversion.py": 12184280074522274780, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/decl_class.py": 6328411894571735935, - "venv/lib/python3.13/site-packages/numpy/_core/overrides.pyi": 13121243505132110607, - "venv/lib/python3.13/site-packages/pygments/lexers/rebol.py": 10460732731157940232, - "docs/api/html/group__DatasetDetailResponse.html": 5915339838963904539, - "backend/src/models/__tests__/test_clean_release.py": 9937664704633848570, - "docs/api/html/group__clean__text.html": 5688432700406149182, - "docs/api/html/group__SupersetClientGetDashboardsPage.html": 14973966407203177325, - "docs/api/html/group__test__dataset__review__scoped__command__returns__confirmation__for__mapping__approval.html": 2557882480884590809, - "venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py": 106471890743211548, - "frontend/postcss.config.js": 5714274776976071678, - "docs/api/html/group__test__patch__dashboard__metadata__replaces__chart__ids.html": 13396557809840355456, - "frontend/src/lib/components/translate/TranslationRunProgress.svelte": 12659537213220151902, - "docs/api/html/group__handleDelete__Function.html": 16605529400179631647, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_read.py": 8825002468745406134, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/__init__.py": 6489437464172324468, - "docs/api/html/group____require__session__version__header.html": 2001910810017263415, - "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/models/installation_report.py": 14534544543677907640, - "frontend/src/lib/api/translate/dictionaries.js": 13928848549982189213, - "docs/api/html/group__trigger__run.html": 4132187777294958921, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_map.py": 4761668276463940924, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/dtype_api.h": 17040779421165903019, - "docs/api/html/group__test__progress__log.html": 16396212906693979688, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/missing.py": 11241703105777668183, - "docs/api/html/group__ProfilePreferencesIntegrationTest.html": 5649513853335976994, - "venv/lib/python3.13/site-packages/pydantic/validate_call_decorator.py": 11130572937843915026, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_dict.py": 4937173601709369119, - "docs/api/html/group__test__legacy__prepare__endpoint__still__available.html": 4064504421429684226, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_analytics.py": 4510712334676848202, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py": 9615571808720893016, - "backend/src/services/clean_release/publication_service.py": 7731809712174358922, - "docs/api/html/group__save__recovery__state.html": 16571398624709231386, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/licenses/LICENSE": 14524289631770661180, - "docs/api/html/group__test__api__key__auth__wrong__permission.html": 546528114053680205, - "docs/api/html/group__SupersetClientExportDashboard.html": 9407120539221335337, - "frontend/src/lib/components/translate/__tests__/test_correction_cell.svelte.js": 7400069550465334990, - "backend/src/api/routes/__tests__/test_assistant_api.py": 14069847490001200210, - "docs/api/html/group__ClarificationOption.html": 4855555471647664817, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_get_numeric_data.py": 11747933921770969706, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____endEvent.html": 8695734456482640318, - "docs/api/html/group__update__environment.html": 9173380704665104139, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authorization_server.py": 13696038495666024258, - "venv/lib/python3.13/site-packages/yaml/scanner.py": 8554164423030397366, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py": 5467043641314706635, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/foo.f90": 14491312943942432024, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptography.py": 4383854709657749855, - "frontend/src/lib/api/translate/corrections.js": 3619404083693055292, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/implicit.py": 7379110378465568776, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_cpp_exception.py": 11851283355840567373, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp2.csv": 10950201682596097452, - "venv/lib/python3.13/site-packages/PIL/FontFile.py": 112053841436523305, - "docs/api/html/group__test__get__schedule__not__found.html": 2262227817332764614, - "docs/api/html/group__getStorageAuthHeaders__Function.html": 16351303303145823090, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/json.py": 4660603129768960095, - "frontend/playwright-report/trace/xtermModule.DYP7pi_n.css": 9346562810570076139, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/array.py": 1919242530821725197, - "docs/api/html/group__SupersetClientGetDashboardsSummary.html": 6037771020479068589, - "docs/api/html/group__upload__file.html": 3260287998952682208, - "venv/lib/python3.13/site-packages/pillow.libs/libwebpdemux-747f2b49.so.2.0.17": 7615380692384566654, - "venv/lib/python3.13/site-packages/annotated_doc/py.typed": 15130871412783076140, - "docs/api/html/group__setAssistantDatasetReviewSessionId__Function.html": 9358397705824505617, - "venv/lib/python3.13/site-packages/passlib/tests/test_registry.py": 15377817946543064293, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/decoder.py": 17559787396729804535, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/fields.py": 8923326131649837922, - "venv/lib/python3.13/site-packages/pygments/lexers/go.py": 11118104397271513417, - "docs/api/html/group__test__parse__dashboard__url__for__filters__native__filters__direct.html": 15481616716847220529, - "docs/api/html/group__TaskType.html": 10021229358037103244, - "backend/src/core/utils/fileio.py": 8848681882907859355, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_ordered.py": 13827016781743929164, - "venv/lib/python3.13/site-packages/passlib/tests/test_apps.py": 10454136535890937237, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isin.py": 8136794647591341784, - "docs/api/html/group__SessionRepositoryMutations.html": 18413228896823064680, - "backend/ss_tools_backend.egg-info/PKG-INFO": 15134841326777953914, - "venv/lib/python3.13/site-packages/pip/_internal/index/package_finder.py": 12631116673181479779, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_npfuncs.py": 8238385897366878558, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/h11_impl.py": 1419719055394402145, - "docs/api/html/group__MaintenanceComponentTests.html": 2439633927977952953, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_compat.py": 11481259393140789881, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_compare.py": 3079569478684646491, - "frontend/src/routes/settings/MigrationMappingsTable.svelte": 574459541553883663, - "venv/lib/python3.13/site-packages/keyring/completion.py": 1027436016070040568, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parse_iso8601.py": 9100947620267156161, - "backend/src/core/utils/async_network.py": 7880952036357722891, - "venv/bin/activate.fish": 5275961582873298085, - "frontend/src/routes/translate/dictionaries/+page.svelte": 1038433841421198720, - "docs/api/html/group__venv__lib__python3__13__site__packages__greenlet__TThreadState__hpp____ThreadState.html": 17071810675918401264, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE": 10253862887434903467, - "docs/api/html/group__test__empty__tables.html": 4751367125471660220, - "venv/lib/python3.13/site-packages/passlib/handlers/__init__.py": 3424087230285514388, - "venv/lib/python3.13/site-packages/packaging/version.py": 13470993734108007779, - "venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py": 15555781487631855604, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setSession.html": 5227957195092266456, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_timedelta.py": 12822713918114715561, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/__init__.py": 15130871412783076140, - "docs/api/html/group__SupersetClientGetDataset.html": 11557014812456007471, - "docs/api/html/group__mask__api__key.html": 16683069110574371100, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/METADATA": 15888766579203341778, - "venv/lib/python3.13/site-packages/yaml/loader.py": 18284689412149643959, - "backend/git_repos/remote/test-repo.git/objects/17/66251d8cb2693136e09ea3def3e776559cb722": 1137762010493457272, - "docs/api/html/group__test__error__state.html": 9960175687355518879, - "venv/lib/python3.13/site-packages/click/py.typed": 15130871412783076140, - "docs/api/html/group__TestSettingsAndHealthSchemas.html": 3643400458745019094, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/integration.py": 13610682193839598457, - "venv/lib/python3.13/site-packages/numpy/_typing/_add_docstring.py": 14657346268890680694, - "venv/lib/python3.13/site-packages/authlib/jose/__init__.py": 317573635113539563, - "venv/lib/python3.13/site-packages/PIL/EpsImagePlugin.py": 1824020964179364491, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_trio.py": 7459901627473962540, - "docs/api/html/group__test__migrate__dashboards__pre__checks.html": 3207110507188780911, - "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.pyi": 15067467792538416274, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_no_public_entities.f90": 12296500845667555672, - "backend/src/plugins/search.py": 13705421801923369220, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/__init__.py": 1948986914452928735, - "docs/api/html/group____get__default__tz__name.html": 1065268266892012697, - "docs/api/html/group__DatabaseResponse.html": 9450948146278252865, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_any_index.py": 10800968419286329268, - "docs/api/html/group__test__get__repository__status__returns__no__repo__payload__for__missing__repo.html": 12740974116041500221, - "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.py": 3001896031623204147, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/LICENSE": 16510279186416777136, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/boxplot.py": 7090586111222952862, - "venv/lib/python3.13/site-packages/numpy/core/numerictypes.py": 5776941405348611027, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_astype.py": 6578539240745332772, - "docs/api/html/folderopend.svg": 12848593963915777968, - "docs/api/html/group__handleDownload__Function.html": 4146074044892202143, - "docs/api/html/navtreeindex4.js": 237051005493385994, - "docs/api/html/group__test__report__models.html": 3181628753533046372, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadEvents.html": 2661201656392934757, - "docs/api/html/group__test__get__log__stats.html": 13273447744955427101, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked_shared.py": 8453174300922339015, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/__init__.py": 15130871412783076140, - "docs/api/html/group__is__masked__or__placeholder.html": 8401491085323739641, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py": 10093932131797570986, - "frontend/build/index.html": 7811861699760349510, - "venv/lib/python3.13/site-packages/pygments/lexers/erlang.py": 11757064262773184741, - "docs/api/html/group__DuplicateJobResponse.html": 4274099317404144755, - "docs/api/html/group__delete__provider.html": 17051246773127866409, - "venv/lib/python3.13/site-packages/pandas/core/reshape/pivot.py": 4736553530021653868, - "frontend/src/lib/components/layout/TopNavbar.svelte": 16154704878262712977, - "venv/lib/python3.13/site-packages/annotated_types/py.typed": 15130871412783076140, - "backend/src/api/routes/environments.py": 6175447476823722833, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/RECORD": 1051307407005554409, - "docs/api/html/group__preview__session__ops.html": 10729427969834726798, - "docs/api/html/group____coerce__query__bool.html": 12206666042007651766, - "venv/lib/python3.13/site-packages/iniconfig/_parse.py": 11418441008427032090, - "docs/api/html/group__generate__uuid.html": 5567603167679991463, - "venv/lib/python3.13/site-packages/passlib/crypto/digest.py": 15890463288512025688, - "backend/src/core/utils/superset_context_extractor/_recovery.py": 11393453671559067169, - "docs/api/html/group__handleValidate__Function.html": 2317767928210351135, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/__init__.py": 15130871412783076140, - "frontend/build/_app/immutable/chunks/Dul6iTE7.js": 10339509389374345579, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_constructors.py": 6727209833819502644, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_snap.py": 4515844385819802784, - "venv/lib/python3.13/site-packages/packaging/licenses/__init__.py": 15879493360260891293, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data": 11957232619107868023, - "backend/src/core/utils/superset_compilation_adapter.py": 13895835815393226662, - "docs/api/html/group__PermissionsTest__Module.html": 11555877868744698217, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_rolling.py": 11118160360072677569, - "backend/src/models/__tests__/test_models.py": 7136192055672203219, - "docs/api/html/group__createAuthStore__Function.html": 15632886189609660791, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_assign.py": 2647902466315482406, - "docs/api/html/group__approval__repository.html": 7564608071995148831, - "docs/api/html/group__DatasourceColumnsResponse.html": 1250225195653267212, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_aarch64_gcc.h": 8140470539757491899, - "venv/lib/python3.13/site-packages/pydantic/plugin/__init__.py": 16803973597469216174, - "backend/src/services/git/_merge.py": 13573452468406399167, - "docs/api/html/group__get__metrics.html": 15776096111007526779, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_mips_unix.h": 17373389962405490482, - "docs/api/html/group__SQLInsertService.html": 818564019015064565, - "venv/lib/python3.13/site-packages/pandas/tests/test_col.py": 14781555956509956371, - "backend/src/services/clean_release/repositories/__init__.py": 10330511711442503831, - "venv/lib/python3.13/site-packages/click/_winconsole.py": 8133229457213704188, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/LICENSE": 6774760218010069963, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_astype.py": 11386658862732393123, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py": 4633931060151989960, - "backend/alembic/README": 17625003422504608299, - "docs/api/html/group__SupersetClientBuildDatasetPreviewQueryContext.html": 18205655956081911529, - "docs/api/html/group__SupersetClientGetCharts.html": 6360695148314682778, - "venv/lib/python3.13/site-packages/attr/setters.pyi": 6670509359139243236, - "docs/api/html/group__TranslateJobTests.html": 10178968892133505657, - "backend/src/services/llm_prompt_templates.py": 16176053862101644239, - "backend/src/plugins/translate/preview_constants.py": 12447768695944862994, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/RECORD": 12555714734722889695, - "venv/lib/python3.13/site-packages/pygments/lexers/tnt.py": 4657130609241407766, - "build.sh": 5842665574427443016, - "docs/api/html/group__MappingCollectionResponse.html": 393143085518008707, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_printing.py": 1901396869745313303, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/strategies.py": 14007486769065345085, - "venv/lib/python3.13/site-packages/pygments/lexers/ooc.py": 14540210212631538007, - "docs/api/html/group__test__get__repository__status__propagates__non__404__http__exception.html": 2565945077269641492, - "backend/src/api/routes/assistant/_history.py": 7072732884181135864, - "venv/lib/python3.13/site-packages/pandas/core/api.py": 3390809109543982410, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/ext.py": 15699809032725838985, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/concatkdf.py": 6712732248328458871, - "docs/api/html/group__unsubscribe__dataset__events.html": 1558763019678339438, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/getlimits.pyi": 93747984191461691, - "docs/api/html/group____normalize__superset__env__url.html": 13235708743999854328, - "venv/lib/python3.13/site-packages/pandas/_libs/internals.cpython-313-x86_64-linux-gnu.so": 2008633361686956377, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/encoder.py": 120921270972591880, - "docs/api/html/group__DatasetReviewApi.html": 5473514434428079682, - "docs/api/html/search/functions_e.js": 16386749146387179095, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/endpoint.py": 13389763148720370546, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/__init__.py": 15130871412783076140, - "docs/api/html/group__ADGroupMapping.html": 9762691734208300582, - "docs/api/html/group__test__get__report__detail__success.html": 15353646901351166916, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_fields.py": 16222559851336781264, - "frontend/src/routes/tools/mapper/+page.svelte": 11733240042848459104, - "docs/api/html/group____build__insert__rows.html": 14245761913362401637, - "backend/src/models/filter_state.py": 9299838116029912439, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_ops.py": 14420421526781215500, - "docs/api/html/group____ensure__llm__validation__results__columns.html": 11439277965253958659, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/METADATA": 9458019064618635438, - "docs/api/html/group____extract__result__payload.html": 866721391193515598, - "docs/api/html/navtreeindex13.js": 13124871548171693035, - "frontend/build/_app/immutable/chunks/BEgtrFG1.js": 8322179021947182552, - "docs/api/html/group__test__rebuild__active__banner.html": 5240773112878811635, - "docs/api/html/group__GitServerConfigBase.html": 14855349343114147443, - "docs/api/html/group__openAssistantChat__Function.html": 13650659288497145081, - "backend/git_repos/remote/test-repo.git/hooks/post-update.sample": 4288868784459664203, - "docs/api/html/group__headers.html": 18390732070560226439, - "docs/api/html/group____resolve__or__create__conversation.html": 8613625745536476404, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_jsonschema_test_suite.py": 12733761462141495972, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/ansi.py": 948249091929588017, - "venv/lib/python3.13/site-packages/urllib3/util/response.py": 6106698559175827207, - "docs/api/html/group__test__task__crash__mid__run__marks__failed.html": 17772358674218724371, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_non_compound.f90": 8048650253889213460, - "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.py": 4154852745131464543, - "docs/api/html/group__candidate__service.html": 9508293729016004404, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____endAllMaintenance.html": 9764396289727492852, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/fallback.py": 4260636031748548345, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict.py": 808180749933361743, - "docs/api/html/group__AsyncSupersetClientModule.html": 9966218695940620420, - "venv/lib/python3.13/site-packages/websockets/sync/router.py": 10335595955948355382, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log10.csv": 13441294827017400897, - "venv/lib/python3.13/site-packages/cryptography/utils.py": 18088508841978595788, - "docs/api/html/group__SourceIntakePanelUxTests__Module.html": 16574757076650646043, - "docs/api/html/group__test__parse__superset__link__dashboard__permalink__partial__recovery.html": 2884738583447431524, - "docs/api/html/group__PluginsRouter.html": 17546343283725473848, - "venv/lib/python3.13/site-packages/anyio/_core/_tempfile.py": 230288423480899091, - "docs/api/html/group__setUser__Function.html": 17905907968752822833, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/LICENSE": 12688029424398428498, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_datetimeindex.py": 14816894350435545024, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__init__.py": 8811477458865810434, - "frontend/src/components/git/ConflictResolver.svelte": 12314421232300049275, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/__init__.py": 8807410829260170597, - "docs/api/html/group__preview__translation.html": 3914934483187354741, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pyx": 11739218099400096619, - "frontend/build/_app/immutable/nodes/21.DfehgIvZ.js": 15936430434341718519, - "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py": 6499914987034257707, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_constructors.py": 3440572832185940666, - "docs/api/html/group__AssistantChatStore.html": 13379737640010123041, - "docs/api/html/group__ClarificationAnswerResultResponse.html": 9302017620017253639, - "docs/api/html/globals_h.html": 17479549949189588951, - "docs/api/html/group__BulkCorrectionSidebar.html": 5126405434557743105, - "docs/api/html/group__resolve__namespace.html": 18066940045737474091, - "venv/lib/python3.13/site-packages/PIL/MpegImagePlugin.py": 7726254767528154514, - "frontend/src/lib/components/reports/__tests__/report_card.ux.test.js": 10328656559485751380, - "docs/api/html/globals_o.html": 15925482099999156324, - "docs/api/html/group__frontend__src__lib__api__validation__js____fetchRuns.html": 486975390462306986, - "venv/lib/python3.13/site-packages/starlette/middleware/sessions.py": 11719848399661632707, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/automap.py": 4672057253309608464, - "docs/api/html/group__archive__exports.html": 9704090435080462719, - "frontend/src/lib/stores/activity.js": 10599836924066718828, - "venv/lib/python3.13/site-packages/pydantic/_internal/_forward_ref.py": 10623110912945011703, - "docs/api/html/group__MergeSpec.html": 4696541116430893567, - "venv/lib/python3.13/site-packages/PIL/ImageSequence.py": 16459886231802763032, - "docs/api/html/group__PreviewPromptBuilder.html": 1054343519842154092, - "backend/tests/test_dashboards_api.py": 14932659980570012781, - "venv/lib/python3.13/site-packages/idna/idnadata.py": 200840860629525970, - "venv/lib/python3.13/site-packages/pandas/_libs/writers.cpython-313-x86_64-linux-gnu.so": 6878855127769203372, - "venv/lib/python3.13/site-packages/pygments/lexers/configs.py": 7712797939104760185, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/memory.py": 9947113720821563254, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/config/test_localization.py": 9524481848419616725, - "docs/api/html/group__resolve__future.html": 11803620953629948836, - "docs/api/html/search/pages_0.js": 9801887205019013612, - "docs/api/html/group__frontend__src__lib__api__validation__js____fetchRunDetail.html": 7905597516894841314, - "docs/api/html/group__SupersetClientInit.html": 12194497519496768637, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/common.py": 10996276696597806957, - "docs/api/html/group__MarkerLogger__reflect.html": 9704040044531782477, - "docs/api/html/group__get__remote__ids__batch.html": 13066542161010026756, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/types.py": 1343652083039949282, - "docs/api/html/group__test__migrate__dashboards__env__not__found.html": 17516663970497083336, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate.py": 11205714006000849926, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/gh18403_mod.f90": 6219951399575860362, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_generic.py": 1098361087956787007, - "venv/lib/python3.13/site-packages/pygments/lexers/boa.py": 14409319610243925864, - "frontend/src/routes/migration/+page.svelte": 10701023521907757906, - "docs/api/html/group__db__session.html": 16354950519275316694, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pymysql.py": 12923876736367229454, - "venv/lib/python3.13/site-packages/jeepney/io/blocking.py": 4699555965372200059, - "venv/lib/python3.13/site-packages/pip/_internal/network/cache.py": 10606522397693824641, - "venv/lib/python3.13/site-packages/urllib3/contrib/__init__.py": 15130871412783076140, - "docs/api/html/group__TranslateJobResponse.html": 12927067343734685379, - "docs/api/html/group__invalidateRepositoryStatuses__Function.html": 3906405379094094913, - "docs/api/html/group__get__all__tasks.html": 11728882406952380263, - "docs/api/html/group__TranslationOrchestrator.html": 6470000776760688783, - "docs/api/html/group____recover__from__markdown.html": 9363288958314889837, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarbuffer.py": 16859789411010045795, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_builtin.py": 74545289993348593, - "venv/lib/python3.13/site-packages/pandas/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_processors.py": 16557117595879208702, - "docs/api/html/group__AuthFixture.html": 17387223620144513588, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp.cpython-313-x86_64-linux-gnu.so": 10263790384087091349, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arrayterator.py": 1646530870049791291, - "docs/api/html/group__test__get__dashboards__profile__filter__contract__owners__or__modified__by.html": 190248188377076802, - "docs/api/html/group__test__native__filter__data__mask__model.html": 14477752184631397987, - "docs/api/html/group__add__environment.html": 5721811152365136259, - "docs/api/html/group__TestLogPersistence.html": 3552045492355387515, - "venv/lib/python3.13/site-packages/httpcore/_backends/mock.py": 6849131425727092784, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_describe.py": 11923715539897559626, - "backend/src/plugins/translate/orchestrator_aggregator.py": 1906675302571034503, - "docs/api/html/group__handleRunMapper__Function.html": 4381627270655982198, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/__init__.py": 16211693040174992594, - "venv/lib/python3.13/site-packages/passlib/tests/tox_support.py": 9200774792427691602, - "docs/api/html/group__TargetSchemaHintTest.html": 14589542991368144718, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_size.py": 14032390806503440750, - "docs/api/html/group__WebSocketLogHandler__emit.html": 18196197399566537987, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/__init__.py": 10470644151952252331, - "docs/api/html/group__ApiModule.html": 8246096891534355769, - "docs/api/html/group__src__plugins.html": 5004920650497586279, - "venv/lib/python3.13/site-packages/PIL/TgaImagePlugin.py": 9208942494595222259, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/__init__.py": 10357835306532281617, - "backend/src/plugins/translate/__tests__/test_dictionary_prompt_builder.py": 12854863376472707556, - "docs/api/html/search/all_6.js": 9451005297611312251, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_time.py": 10463741033950978270, - "venv/lib/python3.13/site-packages/pandas/io/spss.py": 864838144958947906, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/util.py": 18279366777506483176, - "docs/api/html/group__src__core.html": 15005492864993881989, - "docs/api/html/group__test__parsed__native__filters__empty.html": 14083649022229199477, - "venv/lib/python3.13/site-packages/pygments/lexers/jmespath.py": 7426464251945393925, - "docs/api/html/group__get__git__configs.html": 3034817285370598287, - "backend/alembic/versions/9f8e7d6c5b4a_add_is_multimodal_to_llm_providers.py": 11090205342799426943, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_period_index.py": 6634414375581820058, - "docs/api/html/group__log__requests.html": 1562379206737898620, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/RECORD": 1556051687830913019, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_time_grouper.py": 3203729024111501071, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_elements_constructors.py": 9279447066034364952, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____mergeCollectionItem.html": 2019932382006674486, - "venv/lib/python3.13/site-packages/pandas/_libs/algos.cpython-313-x86_64-linux-gnu.so": 15706331654215762588, - "frontend/src/components/git/CommitModal.svelte": 12274093894389435426, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____isDashboardValidationBindingValid.html": 18348584692471214107, - "frontend/src/components/git/GitInitPanel.svelte": 515428146955886062, - "docs/api/html/group__create__api__key.html": 9694652778073542091, - "docs/api/html/group__test__delete__run__success.html": 1498123514744381906, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/proxy.py": 13008557041076030198, - "docs/api/html/group__test__create__job__valid.html": 2035819506657562745, - "venv/lib/python3.13/site-packages/pip/_internal/utils/_log.py": 13011536737418204268, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/accessors.py": 12743680296510533030, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/authorization_server.py": 2491654903543600678, - "venv/lib/python3.13/site-packages/websockets/speedups.cpython-313-x86_64-linux-gnu.so": 10941658763526595989, - "venv/lib/python3.13/site-packages/pydantic/_internal/_git.py": 8760779092674434712, - "docs/api/html/group__test__get__next__executions__invalid.html": 11892479945639081834, - "docs/api/html/group__PermissionsTest.html": 1551075608656490066, - "venv/lib/python3.13/site-packages/pydantic/env_settings.py": 18250976255197243083, - "frontend/build/_app/immutable/chunks/DcMPZ_fa.js": 6501595943098370468, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/METADATA": 6596896092754256988, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/METADATA": 5045041742469375466, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/reporters.py": 10760889726586439724, - "venv/lib/python3.13/site-packages/pandas/core/indexers/objects.py": 409599993084996593, - "docs/api/html/group____admin__user.html": 6240483045330435769, - "docs/api/html/group__test__jinja__comment__excluded.html": 17442480642563518330, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_to_xarray.py": 15341013524814290960, - "docs/api/html/group____prepare__owned__session__mutation.html": 12698201251511839350, - "docs/api/html/group__ComplianceCheckRun.html": 5481441428574708297, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_time_series.py": 5205199092955498507, - "docs/api/html/group__StoresMock.html": 2868613847587767061, - "docs/api/html/group__RepoInitRequest.html": 5997274099282176233, - "docs/api/html/group__LLMAnalysisPlugin.html": 4902416040278143520, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_misc.pyi": 7544619194032232861, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_xport.py": 14080914637071212225, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas_xport.py": 16525568795475918874, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/naming.py": 14503932124671438340, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unstack.py": 11834917665446437163, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/random.pyi": 9425735458050148191, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/RECORD": 8104089156015954779, - "venv/lib/python3.13/site-packages/httpcore/_backends/anyio.py": 6167729964577005634, - "venv/lib/python3.13/site-packages/multipart/decoders.py": 11346752866822986212, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/zip-safe": 15240312484046875203, - "frontend/src/routes/settings/notifications/+page.svelte": 11507547877108528071, - "backend/git_repos/remote/test-repo.git/objects/4f/4e0958b3a42eff67a2b8cfd084d05c43e54716": 6040513978194442637, - "docs/api/html/group__validate__job__preconditions.html": 8970079820851005162, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/RECORD": 11788532857617688351, - "backend/src/services/__tests__/test_resource_service.py": 7247551330585718683, - "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector.py": 15458248567257759500, - "venv/lib/python3.13/site-packages/cffi/error.py": 8147505969912539538, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_finalize.py": 12539815426478689538, - "venv/lib/python3.13/site-packages/passlib/handlers/phpass.py": 16489430001449160264, - "backend/src/plugins/translate/preview_llm_client.py": 14412557858942627249, - "docs/api/html/group__TranslateJobService.html": 14826837477872478202, - "venv/lib/python3.13/site-packages/pandas/tests/extension/date/array.py": 18073740473413382768, - "docs/api/html/group__create__ad__mapping.html": 5919069328535603221, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_des.py": 7198931807116651263, - "docs/api/html/group__ClarificationAnswerDto.html": 15559720501743641230, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_validate.py": 12789481642730855139, - "backend/src/schemas/dataset_review_pkg/_composites.py": 9530969677898630695, - "docs/api/html/group__ReportsApi__Module.html": 17635169210537766774, - "docs/api/html/group__test__prepare__candidate__blocks__external__source.html": 6336322874440712308, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py": 5995058808539530576, - "docs/api/html/group__Role.html": 11260980512040554350, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/RECORD": 1164466882742493155, - "docs/api/html/group__TranslationRunLanguageStats.html": 6103116714421346031, - "frontend/src/lib/components/dataset-review/ValidationFindingsPanel.svelte": 6548767763019176627, - "docs/api/html/group__frontend__src__lib__stores__sidebar__js____setActiveItem.html": 1169479244989646628, - "docs/api/html/group__test__api__key__auth__revoked.html": 11241522851613980961, - "docs/api/html/group__frontend__src__services__storageService__js____listFiles.html": 3465282820892681941, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayterator.py": 3076316311545674897, - "docs/api/html/globals_func_o.html": 8547213010974082815, - "venv/lib/python3.13/site-packages/jaraco/context/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/util/_test_decorators.py": 17930801441803090445, - "docs/api/html/group__load__schedules.html": 3794053930475684249, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/instrumentation.py": 14882045449667214075, - "venv/lib/python3.13/site-packages/starlette/applications.py": 13591448461234020757, - "venv/lib/python3.13/site-packages/pandas/core/methods/selectn.py": 13552176539021625776, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/__init__.py": 237070345717942041, - "docs/api/html/group__TranslateCorrectionsApi.html": 9939568944183336332, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation": 4826455560768665215, - "docs/api/html/group__DictionaryEntryCRUD.html": 6734073441533159576, - "docs/api/html/group____EXT__frontend__ReportTypeProfiles__Test__Module.html": 4055177384498479214, - "docs/api/html/group__TranslationPreviewLanguageResponse.html": 7881198517650898374, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/resource_protector.py": 11539718751174980004, - "docs/api/html/group__PreparePreviewCommand.html": 3982816096514415073, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_item_selection.py": 1027989019709029615, - "venv/lib/python3.13/site-packages/dateutil/easter.py": 14631233890122048084, - "venv/lib/python3.13/site-packages/_pytest/_py/error.py": 2257336434271433543, - "docs/api/html/group__get__latest__preview.html": 13518123627661036753, - "docs/api/html/group__AssistantClarificationIntegrationTest__Module.html": 8857034340039410277, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex.tpl": 8820045022551446255, - "venv/lib/python3.13/site-packages/pip/_internal/exceptions.py": 2252826732781519645, - "venv/lib/python3.13/site-packages/pandas/io/excel/_pyxlsb.py": 10035587889954731645, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/engines.py": 10876954465293959720, - "venv/lib/python3.13/site-packages/pygments/lexers/rnc.py": 9747352274729854620, - "frontend/playwright-report/trace/uiMode.Btcz36p_.css": 7361350017361398233, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/request.py": 17642741955458391050, - "frontend/src/lib/api/translate/runs.js": 1220731879179347641, - "frontend/src/routes/datasets/review/ReviewWorkspaceRightRail.svelte": 13437011381072252917, - "venv/lib/python3.13/site-packages/PIL/PdfImagePlugin.py": 16082854250442815862, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.pyi": 1826323011169529229, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_concat.py": 3788461606817482165, - "docs/api/html/group__make__adapter.html": 7726861153656207298, - "venv/lib/python3.13/site-packages/multipart/exceptions.py": 8580522204565816132, - "docs/api/html/group__TestTaskPersistenceService.html": 12922838788856571301, - "venv/lib/python3.13/site-packages/numpy/lib/array_utils.pyi": 14646405559193539455, - "backend/src/core/plugin_loader.py": 8193009851819399597, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi": 18143251582901996072, - "docs/api/html/group__test__cors__wildcard__default.html": 5002966097368002344, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/create.py": 16693261531832306870, - "venv/lib/python3.13/site-packages/sqlalchemy/util/deprecations.py": 7020540470348910770, - "frontend/src/routes/datasets/__tests__/StatsBar.test.js": 16218511958634350787, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py": 14810883299259169406, - "venv/lib/python3.13/site-packages/pygments/lexers/kusto.py": 16285533608924436308, - "docs/api/html/group__test__start__too__many__tables.html": 8520186301114425960, - "docs/api/html/group__CleanReleaseApi.html": 15834070851858806583, - "docs/api/html/group__test__database__url__env__override.html": 11621545109542978109, - "docs/api/html/group__AssistantHistory.html": 5407469429685717474, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.pyi": 2884703536776830012, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_timedeltaindex.py": 2359681317403710563, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bus.py": 4260827599660763641, - "venv/lib/python3.13/site-packages/PIL/GimpPaletteFile.py": 2040459799887417392, - "docs/api/html/group__test__preview__falls__back__across__matrix__until__supported__endpoint__returns__sql.html": 14400928836202677761, - "docs/api/html/navtree.css": 8480429594731608749, - "frontend/src/routes/settings/EnvironmentsTab.svelte": 13575164255656888464, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawerForTask.html": 5669096331612000811, - "docs/api/html/group__TestSessionConfig.html": 10172148335639053015, - "backend/src/services/reports/report_service.py": 9387534669439738529, - "backend/git_repos/remote/test-repo.git/objects/22/91d2eb86d77accf7401f8952778809cd1aa4d4": 5120428041118096941, - "docs/api/html/group__DatasetReviewSemanticModels.html": 9045404324038154150, - "venv/lib/python3.13/site-packages/pandas/_version.py": 3480474785419232091, - "backend/tests/test_translate_jobs.py": 14073962595360719355, - "docs/api/html/topics.html": 3630804099840379445, - "docs/api/html/group__audit__repository.html": 4149687455544451812, - "venv/lib/python3.13/site-packages/greenlet/TThreadState.hpp": 15599148954148720463, - "docs/api/html/group__ReactivePasswordPrompt__Block.html": 6497383366601912081, - "docs/api/html/group__frontend__src__services__git__utils__js____getSelectedConfig.html": 1957773391502963518, - "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimelike.py": 394614958716301173, - "docs/api/html/group__test__ensure__encryption__key__generates__backend__env__file.html": 17024487701570331364, - "docs/api/html/group__DemoDataService.html": 12469614615958816935, - "venv/lib/python3.13/site-packages/cffi/cffi_opcode.py": 8411344362428947268, - "docs/api/html/group__TranslationPreviewSession.html": 709141522736963142, - "docs/api/html/group__TaskManagerModels.html": 10684990442170315567, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_engines.py": 2790877023773269276, - "venv/lib/python3.13/site-packages/PIL/_tkinter_finder.py": 14045496501810535432, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/apply.py": 3906017176873949738, - "docs/api/html/group__test__superset__api__failure.html": 13822775890788292618, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_table.tpl": 17198355067645888735, - "docs/api/html/group__handlePasswordResume__Function.html": 9536732305476854074, - "docs/api/html/group__normalizePermissionRequirement__Function.html": 3697197038016374909, - "docs/api/html/group__handleMappingResolve__Function.html": 2057230359464505849, - "venv/lib/python3.13/site-packages/starlette/websockets.py": 13834731343216764204, - "docs/api/html/group__required__permission.html": 10075560287490873276, - "docs/api/html/group__get__trace__id.html": 8371547524761107402, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/WHEEL": 7314207500206292683, - "venv/lib/python3.13/site-packages/urllib3/contrib/socks.py": 645086578806871685, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/RECORD": 6349071557579241461, - "backend/tests/core/test_migration_engine.py": 14926155747277116633, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/scalars.py": 3468820466230647963, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_sort.py": 11941630642630249576, - "backend/alembic/versions/e5f4d3c2b1a_add_policy_id_to_validation_results.py": 11168047633691803786, - "venv/lib/python3.13/site-packages/pygments/lexers/algebra.py": 9651893228421050246, - "venv/lib/python3.13/site-packages/pygments/lexers/qvt.py": 1895735583818227972, - "docs/api/html/group____detector__cache__key.html": 17353560702377924027, - "docs/api/html/group__handleEnvChange__Function.html": 18099502322897430177, - "docs/api/html/group__openGitManagerForDashboard__Function.html": 13281219317667598991, - "docs/api/html/search/groups_10.js": 10270802865887856312, - "docs/api/html/group__ValidationStatus.html": 3516721738710393355, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_melt.py": 15663807247682146566, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_core_functionalities.py": 10449747865888948579, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_week.py": 5756727157937218128, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_pickle.py": 2910383574326146698, - "venv/lib/python3.13/site-packages/pandas/io/formats/_color_data.py": 15614976586613738953, - "frontend/src/routes/translate/[id]/+page.svelte": 1911443262444004956, - "docs/api/html/group__BulkFindReplaceRequest.html": 8587097893554395288, - "venv/lib/python3.13/site-packages/numpy/_core/numeric.pyi": 17785360553418386488, - "docs/api/html/group__MigrationMappingsTable.html": 14769683728212158945, - "frontend/src/components/git/GitMergeDialog.svelte": 10276431474456171235, - "venv/lib/python3.13/site-packages/jose/jwt.py": 3405491994268385178, - "venv/lib/python3.13/site-packages/pillow.libs/libbrotlidec-b57ddf63.so.1.2.0": 10032590058372320813, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/asyncmy.py": 11808138763443525443, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/METADATA": 2810323639884879396, - "docs/api/html/group__dry__run__migration.html": 12031214645025182651, - "venv/lib/python3.13/site-packages/git/types.py": 13960696584646279283, - "docs/api/html/group____resolve__candidate__source__version.html": 16340094199420833679, - "backend/src/api/routes/__tests__/test_clean_release_api.py": 8153144254371183034, - "backend/src/plugins/translate/plugin.py": 15783602737040717978, - "backend/git_repos/remote/test-repo.git/objects/c2/491dfd8c841bacd5f491510f6334aa4a626f00": 6297303620364618482, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh17797.f90": 17319074662077763768, - "docs/api/html/group__count__resolved__questions.html": 17204134154082267008, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2py2e.py": 8571347023940385644, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_interface.py": 13648006002292914247, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/base.py": 5924592154431932923, - "docs/api/html/group__ClarificationSessionDto.html": 15558163197172650048, - "docs/api/html/group__StoresMock__Module.html": 1302505229892495674, - "docs/api/html/group__create__temp__file.html": 13523091680298755029, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py": 17060377178521307324, - "docs/api/html/group__believed.html": 3317841457835430076, - "frontend/src/lib/api/maintenance.js": 16247720324991780393, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/RECORD": 12598309574903741388, - "backend/tests/test_logging_audit_fixes.py": 6972807998099938509, - "docs/api/html/group__build__manifest.html": 14209312220395832967, - "docs/api/html/group__record__field__feedback.html": 1747834751721830995, - "docs/api/html/group__ProfilePreference.html": 684032724215963462, - "docs/api/html/group__delete__task.html": 11691883776137464961, - "venv/lib/python3.13/site-packages/python_multipart/__init__.py": 10290895261363712562, - "venv/lib/python3.13/site-packages/numpy/f2py/rules.pyi": 1342380029484127983, - "venv/lib/python3.13/site-packages/dateutil/_version.py": 8279235793001245767, - "venv/lib/python3.13/site-packages/pygments/styles/rrt.py": 9490973359165080360, - "venv/lib/python3.13/site-packages/pip/_internal/models/target_python.py": 422323125651848065, - "venv/lib/python3.13/site-packages/secretstorage/util.py": 1004887530903069622, - "docs/api/html/group__test__multiple__tables.html": 3501237513508769585, - "docs/api/html/group__TestStorageConfigMigration.html": 18428544366449284343, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/asn1.pyi": 16048852379621461316, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_routines.py": 4860173574365060471, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.pyi": 16976386782787920450, - "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/__init__.py": 4290669519959060720, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py": 5324490103277723813, - "docs/api/html/group__run__compliance__run.html": 16684598694182162144, - "venv/lib/python3.13/site-packages/keyring/testing/backend.py": 14295829193445781542, - "venv/lib/python3.13/site-packages/httpcore/_ssl.py": 6665087294869504820, - "backend/src/services/clean_release/__tests__/test_stages.py": 15550789367780507193, - "docs/api/html/group__GiteaRepoSchema.html": 16599628414846816204, - "frontend/src/lib/components/StartMaintenanceForm.svelte": 11114259982483681666, - "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.py": 7365943844063702766, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_scrypt.py": 17378030264314664250, - "docs/api/html/group__test__get__repository__status__batch__returns__mixed__statuses.html": 7606080893793687994, - "docs/api/html/globals_func_b.html": 7416406823814080837, - "venv/lib/python3.13/site-packages/numpy/random/_common.pyi": 5094115692952609118, - "venv/lib/python3.13/site-packages/pydantic_core/py.typed": 15130871412783076140, - "docs/api/html/group__update__banner__on__dashboard.html": 1731887400533112584, - "backend/src/schemas/__tests__/test_settings_and_health_schemas.py": 17933166680828315099, - "backend/src/services/clean_release/repositories/audit_repository.py": 1431160646630812395, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_series_equal.py": 1223890028205011021, - "docs/api/html/search/functions_5.js": 12994729569996994977, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py": 14954780419054588164, - "docs/api/html/search/groups_11.js": 14985601485741813433, - "backend/src/models/translate.py": 3611792946430568934, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/base.py": 3250660921154829815, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_clearing_run_switches.py": 3020294789560975101, - "docs/api/html/group____extract__chart__uuids__from__archive.html": 5294683500183190826, - "docs/api/html/group__test__extract__imported__filters__preserves__clause__level__native__filter__payload__for__preview.html": 14624980795486649087, - "backend/alembic/versions/c4a3a2f74bfe_add_missing_columns_needs_review_.py": 4320606555167902916, - "docs/api/html/group__test__prefix__length.html": 10686431834265921774, - "docs/api/html/group__docker__backend__entrypoint.html": 6509924598611355, - "venv/lib/python3.13/site-packages/itsdangerous/encoding.py": 205863946173486958, - "venv/lib/python3.13/site-packages/PIL/_imagingft.cpython-313-x86_64-linux-gnu.so": 11054435406031360190, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/WHEEL": 7145482834744213753, - "frontend/build/_app/immutable/chunks/B9yoyTb0.js": 16665209953872767073, - "docs/api/html/group__frontend__src__services__adminService__js____updateLoggingConfig.html": 2515261969785467260, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_function.py": 644452398244782315, - "venv/lib/python3.13/site-packages/jsonschema/_types.py": 9250091433305395923, - "backend/git_repos/remote/test-repo.git/objects/3e/05ec6ef6504483c4bc7d00f0d0272c38ec661f": 11182016264585388842, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py": 7423946096970870727, - "docs/api/html/group__GitServerConfigUpdate.html": 4756737950210180794, - "docs/api/html/group__test__success.html": 17787477233526904556, - "venv/lib/python3.13/site-packages/PIL/PsdImagePlugin.py": 4347581352716313298, - "venv/lib/python3.13/site-packages/dateutil/relativedelta.py": 5903095963079475484, - "venv/lib/python3.13/site-packages/referencing/retrieval.py": 11921179805977833120, - "venv/lib/python3.13/site-packages/websockets/http.py": 9406523480132204760, - "docs/api/html/group__TestNormalizeApiError__Class.html": 2259840618123379616, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel_py.py": 9112851333217812996, - "docs/api/html/group__test__get__datasets__env__not__found.html": 15409672532193793329, - "docs/api/html/group__build__detector.html": 17019644481515088584, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/foo.f": 6349567809926050379, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_public_dtype_api_table.h": 8933016749764021181, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_slice.py": 9936085512921529417, - "backend/tests/test_api_key_routes.py": 8620555141760963281, - "venv/lib/python3.13/site-packages/PIL/GbrImagePlugin.py": 7681313333602257802, - "docs/api/html/group__test__simple__select.html": 7841602152481376118, - "frontend/build/_app/immutable/chunks/BU2uRVL9.js": 13186892857011149119, - "venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py": 14416705841810221305, - "docs/api/html/group__ClarificationEngine.html": 9404742488372089320, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/base.py": 13444129650880426353, - "venv/lib/python3.13/site-packages/keyring/backend.py": 14185808762437650951, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fromnumeric.pyi": 8671744104085911610, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/__init__.py": 3619091571279924576, - "docs/api/html/group__build__context__keys.html": 16614569594821867197, - "docs/api/html/group__test__compile__dataset__preview__falls__back__to__chart__data__after__legacy__failures.html": 9366582312624823738, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group____coerce__intent__entities.html": 14588365769904720775, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/backend.py": 9072112660082423691, - "docs/api/html/search/search.css": 7835737499276132321, - "venv/lib/python3.13/site-packages/cryptography/x509/__init__.py": 4066756344049307453, - "docs/api/html/group__deleteAssistantConversation__Function.html": 8387243433121457609, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_cumulative.py": 6346947559103505068, - "venv/lib/python3.13/site-packages/keyring/py.typed": 15130871412783076140, - "backend/src/services/clean_release/repositories/policy_repository.py": 9074701224814573817, - "docs/api/html/group__test__us3__launch__endpoint__requires__launch__permission.html": 13518476876564276937, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_pickle.py": 8705060420848529055, - "docs/api/html/group__SupersetClient.html": 17296693003009978086, - "docs/api/html/group__suggest__mappings__api.html": 3498879923583655660, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_inspect.py": 10573558345297891848, - "docs/api/html/group__test__parse__superset__link__dashboard__partial__recovery.html": 3627304491605755157, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/METADATA": 2053484635654179443, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_nat.py": 18428782698385974380, - "docs/api/html/group__LLMProviderType.html": 16741126194237819812, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_deprecations.py": 16124509063246256847, - "docs/api/html/group__disconnectWebSocket__Function.html": 8352221169150959669, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_salsa.py": 1756536674254931838, - "docs/api/html/group__AssistantClarificationCard.html": 6642464260066671228, - "venv/lib/python3.13/site-packages/pip/_internal/network/auth.py": 6159841209618753296, - "docs/api/html/group____clarification__text__for__intent.html": 15580443210255744848, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_describe.py": 14493300276133670380, - "docs/api/html/group__test__calculate__schedule__empty__list.html": 9256285575620728758, - "docs/api/html/group__GitSchemas.html": 7299943490999314464, - "docs/api/html/group__TestProfileApi.html": 12173172201557312976, - "venv/lib/python3.13/site-packages/jeepney/tests/secrets_introspect.xml": 10717038359151125278, - "venv/lib/python3.13/site-packages/pip/_internal/commands/download.py": 7533669665002975823, - "venv/lib/python3.13/site-packages/keyring/backends/SecretService.py": 17482492072907438453, - "docs/api/html/group__approve__candidate__endpoint.html": 16706174447280477431, - "docs/api/html/group__CandidateArtifact.html": 1360984683406873421, - "docs/api/html/group__ClarificationAnswerCommand.html": 1340014238187838879, - "docs/api/html/group____validate__artifacts.html": 18436640877945920098, - "venv/lib/python3.13/site-packages/rsa/parallel.py": 4968566722541778371, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_collections.py": 14307204206312829238, - "venv/lib/python3.13/site-packages/keyring/backends/macOS/__init__.py": 1010937986753403630, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_from_dummies.py": 5670811108950300761, - "docs/api/html/group__DashboardHub__getValidationBadgeClass__Function.html": 6720343436632169859, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth2_session.py": 7076806577412682835, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/constant_time.py": 2499026008403328245, - "venv/lib/python3.13/site-packages/pygments/filters/__init__.py": 2992007913496948339, - "backend/git_repos/remote/test-repo.git/hooks/pre-rebase.sample": 15693225890270078290, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_policy.py": 13856626567322091253, - "docs/api/html/group__LayoutUtils.html": 6665402780325047543, - "docs/api/html/group____generate__insert__sql.html": 5361897751695942720, - "docs/api/html/group__updateLoggingConfig__Function.html": 5025211689617667851, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/licenses/LICENSE.rst": 5960743202700406649, - "docs/api/html/group__test__metrics__per__language__breakdown.html": 8039163332388559175, - "backend/src/services/validation_run_service.py": 4735086522257772570, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_parameter.py": 15173522343126004527, - "venv/lib/python3.13/site-packages/urllib3/util/ssl_.py": 11720964010284185952, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_3kcompat.h": 5377305123299457337, - "backend/git_repos/remote/test-repo.git/objects/99/38d17c3f4199e53e4d4ebc253fe9c840caa505": 8414792171590415839, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/scalars.pyi": 17683675356659300769, - "venv/lib/python3.13/site-packages/keyring/backends/null.py": 12740996416911604870, - "docs/api/html/group__services.html": 715009395158677164, - "docs/api/html/group__ComplianceRun.html": 6928859539002591467, - "venv/lib/python3.13/site-packages/pydantic/v1/__init__.py": 1642038155370734658, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_numpyconfig.h": 18150055639397773687, - "venv/lib/python3.13/site-packages/attr/validators.py": 4453080662115911998, - "venv/lib/python3.13/site-packages/rapidfuzz/__init__.pyi": 13131938031654453415, - "venv/lib/python3.13/site-packages/pandas/_libs/lib.cpython-313-x86_64-linux-gnu.so": 6452315003728497116, - "docs/api/html/group__test__authenticate__user__updates__last__login.html": 681636108963024398, - "docs/api/html/group__resolve__task.html": 7907422604422526815, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/retry.py": 17371295199208325375, - "docs/api/html/group__test__task__log__level__default.html": 11083738057915848718, - "docs/api/html/group__deploy__dashboard.html": 874940735926289549, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/conftest.py": 17897502508251413988, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_insert.py": 17383986803728810587, - "venv/lib/python3.13/site-packages/numpy/tests/test_public_api.py": 11741922758622705449, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/response.py": 9594165377270752433, - "frontend/build/_app/immutable/entry/app.Cst_W8ea.js": 7511824294684432619, - "docs/api/html/group__DICT__TOKENS__PER__ENTRY.html": 6326579402835354804, - "venv/lib/python3.13/site-packages/h11/_connection.py": 6971907312737339559, - "docs/api/html/group__handleSave__Function.html": 12255691954229374900, - "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.pyi": 2029639875417260268, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_datetimelike.py": 17304541521108700317, - "venv/lib/python3.13/site-packages/anyio/_core/_contextmanagers.py": 1258491775823080427, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/WHEEL": 14550174241288068152, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.cpython-313-x86_64-linux-gnu.so": 1211491156495529145, - "docs/api/html/group__update__yamls.html": 9301161878142210176, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi": 16312114056447366970, - "docs/api/html/group__update__markdown__chart.html": 1789011860209290209, - "venv/lib/python3.13/site-packages/fastapi/dependencies/models.py": 8626894872248212015, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/palette.py": 17383863392192572184, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/poolmanager.py": 5071697837617938064, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py": 7638945613579396718, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/token_endpoint.py": 6118770923176099889, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexer.py": 14400766876373764268, - "venv/lib/python3.13/site-packages/numpy/testing/overrides.pyi": 17408923209056015964, - "docs/api/html/group__test__complete__state.html": 10840390930299689847, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/METADATA": 15970460108484113417, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_resolution.py": 6076761538303762297, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/__init__.py": 15130871412783076140, - "docs/api/html/group__build__initial__profile.html": 17311912746267862536, - "venv/lib/python3.13/site-packages/pandas/core/groupby/__init__.py": 3015263524218708013, - "venv/lib/python3.13/site-packages/pandas/_libs/indexing.pyi": 90995502095752726, - "docs/api/html/group__test__recover__imported__filters__collapses__state__and__metadata__duplicates__into__one__canonical__filter.html": 4764189400381267919, - "backend/git_repos/remote/test-repo.git/hooks/pre-applypatch.sample": 15598058327720909861, - "venv/lib/python3.13/site-packages/pandas/_testing/compat.py": 10172779814048259850, - "docs/api/html/group__isRepositoryReady__Function.html": 3411912494396355874, - "docs/api/html/group__storageService__Module.html": 12420552828308744419, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml_dtypes.py": 18293138335799308439, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_shape_manipulation.py": 12598431412396627042, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_interaction.py": 12216477658994324787, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_util.py": 16024147572807865219, - "docs/api/html/group__flush__task__logs.html": 6410178404460258558, - "venv/lib/python3.13/site-packages/gitdb/test/__init__.py": 18393574323937353933, - "venv/lib/python3.13/site-packages/pygments/lexers/lean.py": 14518351619544450286, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_conversion.pyi": 11580106670226366710, - "docs/api/html/group__SupersetCompilationAdapter.html": 17117078588254804652, - "docs/api/html/group__approve__batch__semantic__fields.html": 3745065335953800486, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py": 9331389900588353667, - "docs/api/html/group____serialize__dataset__review__context.html": 10048110432135787617, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_use.f90": 2657057181044959432, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/multiprocess.py": 10047012560534876149, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE": 16343563559291870811, - "docs/api/html/group__setup__class.html": 16392913197681512262, - "docs/api/html/group____get__or__create__preference__row.html": 5428234839987069116, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/METADATA": 3179724236887811680, - "venv/lib/python3.13/site-packages/jose/jwe.py": 3325660353225624025, - "venv/lib/python3.13/site-packages/yaml/representer.py": 10550136298978225013, - "frontend/src/routes/admin/users/+page.svelte": 6187566471376083204, - "venv/lib/python3.13/site-packages/passlib/handlers/sun_md5_crypt.py": 1949993216707182407, - "docs/api/html/group__DashboardSelection.html": 1501151870893618877, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_retain_attributes.py": 16210262356844956764, - "docs/api/html/group__FilterRecoveryStatus.html": 15412053149747747307, - "docs/api/html/group__test__delete__logs__for__task.html": 15106642576678649464, - "backend/src/api/routes/mappings.py": 2467003850993962558, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/unitofwork.py": 3177255974394221645, - "venv/lib/python3.13/site-packages/pygments/styles/staroffice.py": 11694998942433220413, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nditer.pyi": 33523064068376750, - "docs/api/html/navtreeindex23.js": 17692914288402765747, - "venv/lib/python3.13/site-packages/pydantic/v1/env_settings.py": 6194387033830177219, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_ccalendar.py": 8819357623337533489, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py": 17538029557373507617, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_conversion.py": 9159276697339089788, - "frontend/src/lib/components/translate/BulkReplaceModal.svelte": 9447617465987205060, - "venv/lib/python3.13/site-packages/pygments/lexers/slash.py": 8320676560518108343, - "backend/src/core/task_manager/__tests__/test_context.py": 9922342013893651977, - "venv/lib/python3.13/site-packages/yaml/error.py": 5626374410579058436, - "frontend/e2e/fixtures/global-setup.js": 16032945890934324203, - "docs/api/html/group__get__dashboards__page__async.html": 17651441154400395496, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/properties.py": 5799883762127872919, - "frontend/build/_app/immutable/chunks/CuzmfnQu.js": 4320326097426684598, - "docs/api/html/group__TranslationRecord.html": 17258624764504208878, - "venv/lib/python3.13/site-packages/numpy/random/_generator.cpython-313-x86_64-linux-gnu.so": 15625019745021657670, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/test_decimal.py": 17811784255174057561, - "venv/lib/python3.13/site-packages/pandas/core/groupby/numba_.py": 15997665748332235683, - "venv/lib/python3.13/site-packages/pip/_internal/utils/_jaraco_text.py": 9046755357011730699, - "venv/lib/python3.13/site-packages/numpy/tests/test_lazyloading.py": 17465787495701737655, - "frontend/src/pages/Dashboard.svelte": 16189908777731571070, - "docs/api/html/group__TaskLogRecord.html": 14921887044252207523, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_asfreq.py": 12721047184483392773, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/baked.py": 6911138208603240867, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_setitem.py": 2059440410065814114, - "venv/lib/python3.13/site-packages/pandas/io/parsers/python_parser.py": 7297059565749331767, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_fsspec.py": 6936167344144998172, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/pytestplugin.py": 1591034895268415944, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/__init__.py": 15130871412783076140, - "docs/api/html/group__CheckStageStatus.html": 18359275546300366477, - "docs/api/html/group__version.html": 2015740676777523593, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_select.py": 17823338162428379468, - "docs/api/html/group__test__get__git__configs__masks__pat.html": 6988882008771183656, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/authorization_server.py": 724388765673082720, - "venv/lib/python3.13/site-packages/urllib3/poolmanager.py": 15431313180245757657, - "venv/lib/python3.13/site-packages/numpy/core/arrayprint.py": 9652368112174245943, - "docs/api/html/group__normalizer.html": 9731908195312484209, - "docs/api/html/group__delete__git__config.html": 13270580245597492214, - "venv/lib/python3.13/site-packages/pandas/io/excel/_base.py": 1251545894005373734, - "docs/api/html/group__PreviewCompilationPayload.html": 10856751322722254656, - "docs/api/html/group__getRoles__Function.html": 4843900854760058913, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/licenses/LICENSE": 7959949171752474553, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/histograms.pyi": 9255620158831643201, - "venv/lib/python3.13/site-packages/starlette/datastructures.py": 12097180838087229563, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_round_trip.py": 14684057343786032776, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py": 5997687240967250164, - "docs/api/html/group__DebugPluginModule.html": 18319951556663504137, - "docs/api/html/group__delete__logs__for__task.html": 14542367631920374192, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_argon2.py": 11345706437113628133, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz": 17626265752554406355, - "frontend/src/routes/storage/backups/+page.svelte": 9728305121470326167, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_dispatcher.py": 2063037275809439611, - "backend/src/services/git/__init__.py": 18353922816391181902, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/__init__.py": 12906995259233516197, - "docs/api/html/group__DashboardDetailResponse.html": 10795760041340688929, - "docs/api/html/search/groups_2.js": 4162831969457518591, - "docs/api/html/search/all_e.js": 4022996882705418530, - "docs/api/html/group__handleNavigate__Function.html": 14764981181056434794, - "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5_dep.dot": 17431168212830508462, - "frontend/src/routes/+error.svelte": 9437886843842311915, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_reductions.py": 8258332365408303156, - "venv/lib/python3.13/site-packages/pandas/core/indexes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/api/typing/__init__.py": 6438268275857060114, - "venv/lib/python3.13/site-packages/pygments/lexers/ncl.py": 14838368780033177943, - "frontend/src/routes/settings/__tests__/settings_page.integration.test.js": 4898515600493251972, - "venv/lib/python3.13/site-packages/pip/_internal/utils/datetime.py": 6816292030061301011, - "docs/api/html/group__provider__config__edit__contract__tests__Function.html": 15824301754871063235, - "venv/lib/python3.13/site-packages/pandas/tseries/holiday.py": 1134358927599488799, - "venv/lib/python3.13/site-packages/ecdsa/ssh.py": 17010933326090778158, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_csky_gcc.h": 12929785267195988209, - "docs/api/html/group__test__create__isolated__repository__keeps__mode__data__separate.html": 8351843508860649482, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_datetime.py": 11417836378782978454, - "docs/api/html/group__test__transform__yaml__ignores__unmapped__uuid.html": 2406741085212843540, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.pyi": 16372699564794732463, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_pickle.py": 3419420815386925630, - "docs/api/html/group__test__build__namespaced__id__prevents__cross__mode__collisions.html": 3515946137513241293, - "venv/lib/python3.13/site-packages/pydantic/v1/utils.py": 902648247561165429, - "venv/lib/python3.13/site-packages/numpy/fft/__init__.pyi": 10354990065626209946, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_normalize.py": 11373923576259976755, - "docs/api/html/group__TestComplianceTaskIntegration.html": 13584167952680330678, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_asof.py": 8656532774394927388, - "docs/api/html/group____process__dashboards__for__start.html": 12366425439832314749, - "docs/api/html/group__SupersetContextRecoveryMixin.html": 3332182683532590481, - "docs/api/html/group__create__access__token.html": 2049056774682954094, - "docs/api/html/group__sanitize__username.html": 7200979889774485267, - "docs/api/html/group____dashboard__git__filter__value.html": 5762878789386422054, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py": 17873236864291624476, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimes.py": 8863748366184419339, - "docs/api/html/search/functions_10.js": 7871242740222700745, - "venv/lib/python3.13/site-packages/websockets/typing.py": 13268801935274185287, - "venv/lib/python3.13/site-packages/packaging/requirements.py": 2896326326372719999, - "docs/api/html/group__update__column__description.html": 6565104927390241388, - "frontend/src/components/tasks/TaskLogPanel.svelte": 10382066588776421689, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/METADATA": 15876173086384975957, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_datetimelike.py": 15246580603900811749, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_version.pyi": 3636944488233043883, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_complex.py": 12963449957363121481, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo77.f": 16156304612888138777, - "docs/api/html/group__ProfilePage.html": 7621039567597119091, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_delitem.py": 16538190861492829338, - "backend/src/api/routes/__tests__/test_dashboards.py": 3130878778754818679, - "docs/api/html/group__test__delete__logs__for__tasks.html": 8831029806986025325, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f90continuation.f90": 17056591497459381871, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_string_array.py": 8260883866404398980, - "venv/lib/python3.13/site-packages/keyring/credentials.py": 14515496037842724455, - "venv/lib/python3.13/site-packages/pycparser/__init__.py": 16499940957411260458, - "backend/src/api/routes/__tests__/test_git_status_route.py": 4501452711469218555, - "docs/api/html/group__LastTask.html": 9510396811518924648, - "docs/api/html/group__ReportsFilterPerformanceTest__Module.html": 6300674926783924392, - "docs/api/html/group__MaintenanceStore__state.html": 4681812142330433679, - "venv/lib/python3.13/site-packages/fastapi/types.py": 3867821246650502267, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_openid.py": 8708980645667847439, - "docs/api/html/group__SessionEventLoggerModule.html": 766072337035773703, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/base.py": 15872835330892203433, - "docs/api/html/group__test__add__and__remove__translation__job.html": 2228611743163264158, - "docs/api/html/group__handleTest__Function.html": 17441043050901879377, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/styled.py": 8726407298858811635, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_level_values.py": 12490886203425393290, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numeric_only.py": 13610631681447961840, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_comment.py": 5603075924832933306, - "docs/api/html/group__fetchDatabases__Function.html": 8786717975484135660, - "venv/lib/python3.13/site-packages/urllib3/util/timeout.py": 17922362393077965047, - "docs/api/html/group__test__get__run__detail__not__found.html": 9435086798292174243, - "venv/lib/python3.13/site-packages/pygments/lexers/dalvik.py": 5549983892200382076, - "docs/api/html/group__test__auth__db__dev__fallback__works.html": 4992951578259739834, - "docs/api/html/group__MapColumnsRequest.html": 17510027445054962, - "docs/api/html/group__lookup__superset__accounts.html": 11041872730104611728, - "backend/tests/test_translate_scheduler_guard.py": 13396608843530344156, - "venv/lib/python3.13/site-packages/pydantic/main.py": 14086938579850319884, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log.csv": 221209423454319618, - "docs/api/html/group__type__profiles.html": 8266323522750752760, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/base.py": 13634605726041717070, - "docs/api/html/group__pop__span.html": 10448187202370662718, - "backend/src/core/utils/__init__.py": 12454773556575864508, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____endMaintenance.html": 8307569460468971226, - "docs/api/html/group__get__plugin.html": 13473880094770668887, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.py": 1948707545068786, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/__init__.py": 11157967642611612522, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_factorize.py": 861897342553280506, - "venv/lib/python3.13/site-packages/jaraco/classes/properties.py": 6976928770004942858, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_fillna.py": 13675584998644907265, - "docs/api/html/group__TaskHistory.html": 12837645096150183863, - "venv/lib/python3.13/site-packages/pydantic/experimental/arguments_schema.py": 8644805798814234457, - "backend/src/services/clean_release/policy_engine.py": 7078053766918689048, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____listDashboardBanners.html": 6341650642747001563, - "venv/lib/python3.13/site-packages/pandas/core/arrays/numeric.py": 17834058423729918687, - "docs/api/html/group____build__context__keys.html": 703884142675670583, - "venv/lib/python3.13/site-packages/attr/__init__.pyi": 13203080513025719049, - "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.py": 11397521081216402594, - "venv/lib/python3.13/site-packages/pygments/lexers/meson.py": 14659991367778746658, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html.tpl": 12550218314797699304, - "docs/api/html/group__test__api__datasource__columns__bad__env.html": 6521752800637051274, - "venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py": 8159971229245441880, - "docs/api/html/group__run__check__legacy.html": 14080804246720725799, - "venv/lib/python3.13/site-packages/greenlet/_greenlet.cpython-313-x86_64-linux-gnu.so": 6036818030377883416, - "venv/lib/python3.13/site-packages/httpcore/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/poly1305.py": 10854598965202354516, - "docs/api/html/group__test__api__duplicate__job__not__found.html": 18058786778280506982, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_replace.py": 8166203537440976347, - "docs/api/html/group__GitPluginExt.html": 959273690760699620, - "docs/api/html/group__test__mobile__functions__Function.html": 10962243893129004667, - "docs/api/html/group__apply__retention__policy.html": 3669478677806189613, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/python.py": 17886036670342116501, - "venv/lib/python3.13/site-packages/numpy/ma/LICENSE": 6723325434476453127, - "docs/api/html/group__SupersetDatasetsPreviewMixin.html": 9433049209236361670, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh2848.f90": 5799106109999071412, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_searchsorted.py": 11161416745923343741, - "venv/lib/python3.13/site-packages/urllib3/util/util.py": 12490259045660543460, - "docs/api/html/group__test__patch__profile__preferences__cross__user__denied.html": 11110530851215495265, - "venv/lib/python3.13/site-packages/requests/structures.py": 2668010839316715865, - "docs/api/html/group__PreviewExecutor.html": 5786108329700439190, - "backend/src/services/git/_status.py": 13381104725964137243, - "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.pyi": 13945478633722616250, - "docs/api/html/group__test__execution__snapshot__includes__recovered__imported__filters__without__template__mapping.html": 4769785789675369801, - "venv/lib/python3.13/site-packages/pygments/lexers/nimrod.py": 8457135214131458774, - "docs/api/html/group__select__next__open__question.html": 6182628715898661160, - "venv/lib/python3.13/site-packages/rapidfuzz/__init__.py": 12462153783886855740, - "docs/api/html/group__health__router.html": 16305034473081450586, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas7bdat.py": 8963983384208121526, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/wsproto_impl.py": 3072714633272661847, - "docs/api/html/group__MergeStatusSchema.html": 12600052237122559158, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5support-d0bcff84.so.0.1": 16819924515291719993, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/applicator": 6870991911243684132, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_kwargs.py": 10173906126211473463, - "docs/api/html/group__ServiceUtils.html": 9023919090808833531, - "venv/bin/websockets": 12099863324306500376, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_case_when.py": 4133174129660834805, - "docs/api/html/group__handleAddOrUpdateEnv__Function.html": 3373509982933940852, - "docs/api/html/group__SupersetClientResolveExportFilename.html": 4965049196551993810, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__MaintenanceEvent.html": 7096871059783087964, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_delete.py": 3485374671877063217, - "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.py": 16856718240972376883, - "venv/lib/python3.13/site-packages/pygments/lexers/macaulay2.py": 8022891778577314212, - "venv/lib/python3.13/site-packages/greenlet/slp_platformselect.h": 10428201768098464386, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/aiomysql.py": 12999962380333482189, - "docs/api/html/group__remove__translation__job.html": 17823493926961254166, - "docs/api/html/group__ComplianceOrchestrator.html": 1898684865008336902, - "docs/api/html/group__GitServiceBranchMixin.html": 12313083573663321996, - "docs/api/html/group__MockGitService.html": 15745917040920989657, - "docs/api/html/group__handleInitializeRepositories__Function.html": 4127737207323707916, - "frontend/build/_app/immutable/nodes/32.998cmedr.js": 7475181462383722002, - "docs/api/html/group__AuthSchemas.html": 10571020082050700369, - "venv/lib/python3.13/site-packages/numpy/_core/lib/libnpymath.a": 17555816778997028283, - "docs/api/html/group__docker__nginx__ssl__conf.html": 6000110961620180556, - "venv/lib/python3.13/site-packages/greenlet/tests/test_tracing.py": 10227316489696807112, - "venv/lib/python3.13/site-packages/pandas/core/computation/engines.py": 1889823242704769952, - "docs/api/html/group__test__active__default__true.html": 1870632923808868585, - "docs/api/html/group__get__log__stats.html": 6749080858232448787, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_constructors.py": 11065058462752831997, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/_pytest/unittest.py": 10848803633530338204, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/packages.py": 11689856177725665974, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape_base.pyi": 10291383658131961346, - "docs/api/html/group__call__llm.html": 18394029930905917337, - "docs/api/html/group__ValidationPolicyBase.html": 9049806250569046639, - "venv/lib/python3.13/site-packages/pygments/lexers/webidl.py": 8097260111959278117, - "docs/api/html/group__fetch__paginated__data.html": 2029155159485988556, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/package_data.py": 3078502124870585867, - "venv/lib/python3.13/site-packages/cryptography/x509/verification.py": 1079694784860492682, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/watchfilesreload.py": 13062440477475586751, - "venv/lib/python3.13/site-packages/git/index/util.py": 5022401275553420722, - "docs/api/html/group__is__string__literal.html": 4316654409980882843, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_algs.py": 2479791018067637690, - "venv/lib/python3.13/site-packages/numpy/_core/_operand_flag_tests.cpython-313-x86_64-linux-gnu.so": 12296711931541109624, - "docs/api/html/group__frontend__src__services__adminService__js____updateRole.html": 1298256109882565356, - "docs/api/html/group__ValidationResult.html": 15773352481010235482, - "backend/alembic/versions/543d43d752b8_migrate_old_dictionary_entries_and_.py": 10988663686444687907, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24008.f": 3872245077994936272, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/models.py": 16636119441941357281, - "venv/lib/python3.13/site-packages/jeepney/tests/test_auth.py": 15125055415702968414, - "docs/api/html/group__update__job.html": 3867501244194585555, - "venv/lib/python3.13/site-packages/rpds/rpds.cpython-313-x86_64-linux-gnu.so": 561326734302492859, - "venv/lib/python3.13/site-packages/tzlocal/utils.py": 7893867974548929154, - "venv/lib/python3.13/site-packages/ecdsa/test_ecdsa.py": 17595025859105321562, - "venv/lib/python3.13/site-packages/click/termui.py": 7022374827798605437, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_timegrouper.py": 276805119790266454, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_repr.py": 11737029058271003097, - "venv/lib/python3.13/site-packages/pygments/lexers/ampl.py": 4594260378653369985, - "docs/api/html/group__test__abort__controller.html": 12820594964734088755, - "docs/api/html/group__test__returns__existing__banner.html": 12335002589831108201, - "docs/api/html/group__ApiHelper.html": 11728817047769872638, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/client_auth.py": 3301527187487914220, - "venv/lib/python3.13/site-packages/pip/_internal/commands/__init__.py": 3795218871212151351, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE": 14316979437290727897, - "backend/git_repos/remote/test-repo.git/objects/59/3a37acec7d2bddce10df90ab17bc9ede52e523": 1117161574576783985, - "docs/api/html/group__health__service.html": 14616989535045356318, - "docs/api/html/navtreeindex25.js": 18335817056234416468, - "venv/lib/python3.13/site-packages/websockets/protocol.py": 6401227903827084901, - "docs/api/html/group__TaskLog.html": 12777742835079365241, - "backend/src/core/task_manager/context.py": 11799776870777277331, - "docs/api/html/group__notifications.html": 16884491307342259092, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_julian_date.py": 6868080396693831272, - "backend/tests/test_api_key_model.py": 18059967072469014911, - "docs/api/html/group__TraceContextMiddleware______init____.html": 1201832506877617610, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/infer.py": 12541904372617915365, - "venv/lib/python3.13/site-packages/httpcore/_utils.py": 3255581092836273826, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_ndarray.py": 6863874753941288585, - "docs/api/html/group__end__all__maintenance.html": 3898926746936500242, - "venv/lib/python3.13/site-packages/pip/_internal/network/xmlrpc.py": 5064964778750366059, - "docs/api/html/group__GitServerConfigSchema.html": 14623914455240364180, - "docs/api/html/group__test__acceptPreview__Function.html": 12813709054328235897, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/__init__.py": 11526998905763262175, - "docs/api/html/group__LaunchStatus.html": 3552441502967473600, - "backend/src/services/clean_release/repositories/report_repository.py": 8327339942479580975, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_regression.py": 12593643817685777170, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_missing.py": 7034102519042351965, - "docs/api/html/group__frontend__src__services__storageService__js____downloadFile.html": 13909418440035321568, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/emscripten_fetch_worker.js": 10530710876604536879, - "docs/api/html/group__frontend__src__services__taskService__js____clearTasks.html": 16739125797009486352, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/well_known.py": 8259335680633926377, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_real.py": 5283415082530228716, - "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.pyi": 2104100025130695295, - "docs/api/html/group__dataset__review.html": 12837365615967684273, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/RECORD": 5169907629260560698, - "docs/api/html/globals_i.html": 10148981415694925638, - "venv/lib/python3.13/site-packages/numpy/random/_mt19937.pyi": 15904463104068500379, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_real.f90": 9097998998863007705, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py": 11393911418463808195, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rank.py": 14115215895836854973, - "docs/api/html/group__test__no__prior__run__returns__all__rows.html": 3777590034380927800, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_asymmetric.py": 6903778603779934572, - "docs/api/html/group__TranslateDictionaryRoutesModule.html": 3644389177415562158, - "docs/api/html/group__sanitize__filename.html": 18087990957535401473, - "docs/api/html/group__ClarificationOptionDto.html": 16094217856744339801, - "venv/lib/python3.13/site-packages/attr/filters.pyi": 13942974755090135724, - "venv/lib/python3.13/site-packages/apscheduler/executors/debug.py": 14072221218785494847, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py": 17765129030290410902, - "venv/lib/python3.13/site-packages/anyio/streams/file.py": 15232345967545920863, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/markers.py": 5247208668583898949, - "venv/lib/python3.13/site-packages/numpy/lib/format.py": 14971167445246135440, - "docs/api/html/group__build__default__preference.html": 9555424369113315940, - "docs/api/html/group__PROVIDER__DEFAULTS.html": 865392888129938339, - "venv/lib/python3.13/site-packages/_pytest/debugging.py": 16135285365306979060, - "docs/api/html/group__test__add__validation__job.html": 17097612352066161020, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctan.csv": 4581185622290535488, - "venv/lib/python3.13/site-packages/PIL/XpmImagePlugin.py": 6644626557482464312, - "venv/lib/python3.13/site-packages/starlette/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/internals/blocks.py": 6448406236246476333, - "docs/api/html/group__task__id.html": 14850347989291467417, - "venv/lib/python3.13/site-packages/pandas/plotting/__init__.py": 13230487326686617492, - "docs/api/html/group____make__httpx__status__error.html": 1809671805993904222, - "venv/lib/python3.13/site-packages/pygments/lexers/kuin.py": 2818054539831699366, - "venv/bin/keyring": 1488968652276547659, - "docs/api/html/search/all_1.js": 5807190646834111663, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py": 16386268386376675175, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py": 7732330576029516854, - "docs/api/html/group____commit__owned__session__mutation.html": 16349315849175268536, - "backend/src/plugins/git_plugin.py": 5910855495971245586, - "docs/api/html/group__AppDependencies.html": 8553210380267321705, - "venv/lib/python3.13/site-packages/cffi/backend_ctypes.py": 2042566833861899211, - "docs/api/html/group__MaintenanceEventStatus.html": 8449077302738724154, - "venv/lib/python3.13/site-packages/pip/_internal/cli/command_context.py": 9426036980437931533, - "docs/api/html/group__get__role__by__id.html": 5025478251031829538, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/json.py": 15373167408891671190, - "docs/api/html/group__test__us3__preview__response__propagates__refreshed__session__version__for__launch__follow__up.html": 14305667278559815270, - "venv/lib/python3.13/site-packages/numpy/tests/test_configtool.py": 8208415015478205502, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/types.py": 16795741817129898714, - "backend/git_repos/remote/test-repo.git/objects/d5/0298a42699f39e4601c0aa23eab07bcb0703c7": 1966124163198751340, - "docs/api/html/group__test__empty__key__cols__returns__all__rows.html": 72705144746688470, - "venv/lib/python3.13/site-packages/fastapi/openapi/docs.py": 16735218221639179927, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_cisco.py": 1786475632318043669, - "docs/api/html/group____is__production__env.html": 8724827897681230971, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/__init__.py": 1798074863773727377, - "docs/api/html/group__translate__router.html": 11232553803187505597, - "docs/api/html/group__AuthRepository.html": 8945062834514291149, - "venv/lib/python3.13/site-packages/pip/__init__.py": 9449893869469432503, - "venv/lib/python3.13/site-packages/pygments/lexers/devicetree.py": 9820277168995231927, - "docs/api/html/group__test__delete__git__config__removes__record.html": 6921631931687420246, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_api_info.py": 16491629653122827368, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_macosx.h": 3653129798702716366, - "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.pyi": 6576645822227068922, - "docs/api/html/group__ValidationRecord.html": 5281046073662609348, - "backend/src/plugins/translate/dictionary_crud.py": 17858252266960667195, - "docs/api/html/group__GitInitPanel.html": 15661107424053425656, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/flow_control.py": 4822804840057360506, - "frontend/src/routes/dashboards/[id]/+page.svelte": 2328002918662170835, - "venv/lib/python3.13/site-packages/pygments/formatters/__init__.py": 8009871891954370829, - "venv/lib/python3.13/site-packages/apscheduler/triggers/interval.py": 3964550092873794670, - "docs/api/html/group__TestLoggingAuditFixes.html": 525186929866307297, - "docs/api/html/group__execute__migration.html": 4270006163430911897, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_frequencies.py": 14039315221824123200, - "venv/lib/python3.13/site-packages/starlette/middleware/gzip.py": 4321371046149637632, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_searchsorted.py": 11208047646304753715, - "docs/api/html/group__TestTranslateExecutorFilter.html": 7954474206063805341, - "docs/api/html/group__test__confirmation__owner__mismatch__returns__403.html": 2005624954638348501, - "venv/lib/python3.13/site-packages/pip/_vendor/typing_extensions.py": 10158933506062403688, - "frontend/build/_app/immutable/chunks/CZV46x5e.js": 6902302160092901507, - "venv/lib/python3.13/site-packages/rsa/randnum.py": 13958355153634534526, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.cpython-313-x86_64-linux-gnu.so": 7840846965295537844, - "frontend/playwright-report/trace/assets/urlMatch-BYQrIQwR.js": 8070494174996726626, - "frontend/src/lib/components/settings/ApiKeysTab.svelte": 17208249963941978218, - "docs/api/html/group__wait__for__resolution.html": 4445289133815184420, - "docs/api/html/group__TestTextCleaner.html": 16532067994218161464, - "frontend/build/favicon.png": 16740551781088792605, - "docs/api/html/group__LogFilter.html": 10465257837287313695, - "backend/src/plugins/translate/preview_session_ops.py": 2497494071534748391, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/__init__.py": 10456333337205488584, - "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiPost.html": 16080559730888993459, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix_py.py": 17041764258638228993, - "venv/lib/python3.13/site-packages/pandas/core/accessor.py": 1188716996113179524, - "docs/api/html/group__MaintenanceSettingsResponse.html": 15485267918904014972, - "venv/lib/python3.13/site-packages/pandas/tests/test_common.py": 8987965560487646705, - "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js": 7546622841909210622, - "docs/api/html/group__test__compile__dataset__preview__prefers__legacy__explore__form__data__strategy.html": 15164673100544799582, - "venv/lib/python3.13/site-packages/pandas/core/interchange/buffer.py": 12961802206383700922, - "docs/api/html/group__upsert__clarification__finding.html": 8857419435924114441, - "docs/api/html/open.png": 3773965092868909301, - "docs/api/html/group__createTask__Function.html": 15327860435941970591, - "frontend/src/lib/stores.js": 8225317698711142177, - "venv/lib/python3.13/site-packages/greenlet/TGreenlet.cpp": 4121171004360760212, - "venv/lib/python3.13/site-packages/pandas/io/formats/xml.py": 12235904458551067063, - "docs/api/html/group__MaintenanceEndTaskResult.html": 18181177616321427742, - "venv/bin/pyrsa-priv2pub": 16766314338551408621, - "docs/api/html/group__openAssistantChatWithContext__Function.html": 6437289261413282510, - "venv/lib/python3.13/site-packages/_pytest/config/findpaths.py": 7867603756389940562, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/compiler.py": 13924519162400330589, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_doc.py": 7865575374100528149, - "backend/src/core/auth/repository.py": 15675496219240601253, - "venv/lib/python3.13/site-packages/passlib/handlers/scrypt.py": 10089372287597257402, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/__init__.py": 6654484356035994666, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/provision.py": 17812733620485633975, - "docs/api/html/group__clean__release__repositories.html": 4346082490646294177, - "docs/api/html/group______init____.html": 12200029640079662818, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_getlimits.py": 118041882543413669, - "dist/docker/frontend.0.1.0.tar.xz": 16011357094675100098, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py": 10843527833285941674, - "docs/api/html/group__test__list__runs__invalid__status.html": 987375723925520985, - "docs/api/html/group__ReviewWorkspaceRightRail.html": 6482365234920032721, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_unique.py": 3989602816674881215, - "docs/api/html/group__test__jinja__set__block.html": 1391771195083382074, - "docs/api/html/group__has__permission.html": 1685491093213182094, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pct_change.py": 10313980265441433173, - "docs/api/html/group__MergeConflictFileSchema.html": 6451830131202489447, - "docs/api/html/group__test__validation__policy__create__accepts__structured__custom__channels.html": 12324191215161408510, - "venv/lib/python3.13/site-packages/fastapi/routing.py": 466387159689567541, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/util.py": 1520899420719605380, - "venv/lib/python3.13/site-packages/attr/_config.py": 1864824728504480204, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/block.f": 10727418304254548946, - "venv/lib/python3.13/site-packages/git/compat.py": 16843345645628515492, - "frontend/src/routes/datasets/+page.svelte": 4090458050277453670, - "frontend/build/_app/immutable/chunks/B92OcwSl.js": 16271941207641380171, - "docs/api/html/group__run__compliance__status.html": 18273707357208548255, - "docs/api/html/group__Card.html": 8557385791478592213, - "docs/api/html/group__has__plugin.html": 2360443021065095729, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.py": 17112989332785349705, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite_e.py": 8912795372542950449, - "frontend/build/_app/immutable/nodes/27.b6yAZh1u.js": 1743695304248018897, - "docs/api/html/group__BulkReplacePreviewItem.html": 14430839695131325196, - "backend/tests/test_logger.py": 1680169894013597024, - "docs/api/html/group__IdMappingServiceModule.html": 13805743174711201381, - "docs/api/html/group__GitRepositorySchema.html": 13549814497751486913, - "docs/api/html/group__DatasetProfile.html": 5331057226299357274, - "venv/lib/python3.13/site-packages/greenlet/TStackState.cpp": 16720079760074325527, - "venv/lib/python3.13/site-packages/pip/_internal/build_env.py": 3614358934066127900, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polybase.pyi": 3404315099794022355, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_formats.py": 13752366280743004410, - "venv/lib/python3.13/site-packages/pygments/lexers/_lasso_builtins.py": 4006621424665751347, - "docs/api/html/group__list__runs.html": 16735737419042964016, - "docs/api/html/group____generate__banner__id.html": 4235550462258700579, - "docs/api/html/group__test__us2__clarification__endpoints__persist__answer__and__feedback.html": 8985005200241520206, - "docs/api/html/group__TextCleaner.html": 4066249505895592095, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_concat.py": 10207975532842927134, - "docs/api/html/group__SQLGenerator.html": 14160413998657267182, - "venv/lib/python3.13/site-packages/pip/_internal/commands/show.py": 11839987586497822300, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.py": 5564496700647900501, - "frontend/build/_app/immutable/chunks/_Pf3rQig.js": 13183249556030654823, - "docs/api/html/group____load__db__mapping.html": 5085581099428529195, - "venv/lib/python3.13/site-packages/packaging/tags.py": 1999137344425005827, - "docs/api/html/group__revoke__api__key.html": 5362512618088609082, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_regression.py": 16266600037241221681, - "venv/lib/python3.13/site-packages/websockets/__main__.py": 11948055520555854616, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ranges.py": 14549386056660231279, - "docs/api/html/group__TranslationRunGlobalIndicator.html": 7439172912927142076, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/halffloat.h": 16913381775994652343, - "venv/lib/python3.13/site-packages/click/globals.py": 18353671282454331523, - "venv/lib/python3.13/site-packages/h11/_util.py": 9362339074207904329, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/licenses/LICENSE": 5563356551734678258, - "docs/api/html/group__create__gitea__repository.html": 8308843234769151760, - "docs/api/html/group__TargetSchemaHint.html": 10951669208141820307, - "docs/api/html/group__frontend__src__services__adminService__js____deleteRole.html": 151266335644835279, - "venv/lib/python3.13/site-packages/pygments/lexers/tlb.py": 1169242051301155885, - "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.py": 1262543237476191714, - "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.py": 14696841109142419143, - "docs/api/html/group__environmentContext__Store.html": 12330215193574148972, - "venv/lib/python3.13/site-packages/numpy/lib/npyio.py": 6782361985454523908, - "docs/api/html/group__MapperToolPage.html": 15826560549177500594, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/meson.build.template": 9935732744553086652, - "venv/lib/python3.13/site-packages/psycopg2/extras.py": 7510107741729866600, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/util.py": 3591114320020964752, - "venv/lib/python3.13/site-packages/pydantic/annotated_handlers.py": 10186120300163345360, - "docs/api/html/group__frontend__src__services__storageService__js____encodeStoragePath.html": 13531232492438542038, - "docs/api/html/group__DatasetReviewEntryUxTests.html": 11847231912591045967, - "backend/src/services/profile_utils.py": 11110559205027128244, - "backend/alembic/versions/86c7b1d6a710_add_is_admin_column_to_roles_table.py": 7648823106726571577, - "venv/lib/python3.13/site-packages/anyio/lowlevel.py": 11919467357746648592, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/__init__.py": 16532856661318424631, - "venv/lib/python3.13/site-packages/PIL/PpmImagePlugin.py": 7107324729714470853, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pxd": 14261507718442469988, - "docs/api/html/group__test__metric__snapshot__stores__per__language.html": 12291939914587003915, - "docs/api/html/group__SecurityBadgeService.html": 10559304755960949908, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.py": 15657897994221955882, - "docs/api/html/group__test__delete__task__runs__default__false.html": 12317988397081006520, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/errors.py": 2147619440137604907, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets.py": 4499538899492148499, - "docs/api/html/group__test__update__git__config__modifies__record.html": 2052816531421607126, - "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators.py": 9928398334052202841, - "docs/api/html/group__test__tui__real__mode__bootstrap__imports__artifacts__catalog.html": 6102024140189662129, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/argon2.py": 15389474901714096890, - "backend/tests/test_security_audit_fixes.py": 10329229098676202553, - "docs/api/html/group__test__cli__candidate__register__scaffold.html": 18082978099842834637, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_categorical.py": 8844412630511751140, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py": 1129954411080617001, - "docs/api/html/group__ScheduleAtAGlance.html": 7804111443901031953, - "docs/api/html/group__create__mapping.html": 1348589188703029518, - "docs/api/html/group__test__list__runs__pagination.html": 11299418008517910530, - "venv/lib/python3.13/site-packages/tzlocal/__init__.py": 11017072814026497364, - "docs/api/html/group____load__plugins.html": 2375668507770172038, - "docs/api/html/group__test__persist__task__resolves__environment__slug__to__existing__id.html": 12362861300562934210, - "frontend/playwright-report/trace/manifest.webmanifest": 565917384317025203, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/METADATA": 11419135077900073883, - "docs/api/html/group__MaintenanceEndAllTaskResult.html": 2276064404712323484, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_head_tail.py": 13047338070883375426, - "docs/api/html/group__goBackToList__Function.html": 7594473002237426910, - "docs/api/html/group__schedule__dashboard__validation.html": 18088681835965473182, - "venv/lib/python3.13/site-packages/jeepney/__init__.py": 1063800617763464773, - "docs/api/html/group__APIKeyModel.html": 7167590347437218348, - "docs/api/html/group__test__environment__connection.html": 7656214946341009354, - "docs/api/html/group__test__sql__lab__launch__falls__back__to__legacy__execute__endpoint.html": 3611882124869363, - "venv/lib/python3.13/site-packages/ecdsa/_version.py": 17707102226558781833, - "venv/lib/python3.13/site-packages/referencing/typing.py": 8296636464219681747, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/rec.pyi": 14615751247243515028, - "venv/lib/python3.13/site-packages/pandas/core/reshape/reshape.py": 5387892304726762127, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_setops.py": 15365064654930699428, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_longtable.tpl": 12723621983797415444, - "docs/api/html/group____handle__network__error.html": 15557655064289466036, - "venv/lib/python3.13/site-packages/anyio/__init__.py": 12752386096259972231, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_format.py": 5231304674283854194, - "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/__init__.py": 3521833849634327520, - "venv/lib/python3.13/site-packages/pygments/styles/dracula.py": 12379322008538174078, - "docs/api/html/group__ReportQuery.html": 17989748081758748935, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/errors.py": 10750605030539582738, - "venv/lib/python3.13/site-packages/pygments/lexers/_stan_builtins.py": 1961875544235948177, - "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__reference.html": 7880890145565202782, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/RECORD": 3205266407680661753, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-2.csv": 4052796393463357888, - "docs/api/html/group__ClarificationHelpers.html": 2159036796456300532, - "frontend/build/_app/immutable/chunks/DV17vNYu.js": 5906252992532056558, - "backend/tests/test_constants_audit_fixes.py": 132399385608717170, - "docs/api/html/group__test__api__delete__job__not__found.html": 9283924924862498850, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_extension.py": 17245018204352082978, - "docs/api/html/group__cancel__operation.html": 16696836689617631015, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_fillna.py": 8802093795040069423, - "docs/api/html/group__AsyncSupersetClientInit.html": 2528317931875935210, - "venv/lib/python3.13/site-packages/h11/_receivebuffer.py": 4631433251046338381, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/common.py": 11384497819737275399, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeSupersetBaseUrl.html": 9936225426251260789, - "venv/lib/python3.13/site-packages/_pytest/pathlib.py": 9719734558816066453, - "venv/lib/python3.13/site-packages/pydantic/_internal/_generate_schema.py": 7343380095251236503, - "venv/lib/python3.13/site-packages/pydantic_settings/__init__.py": 4374954204580208673, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype.pyi": 17662618580381221543, - "docs/api/html/group__clean__db.html": 17613548754885493168, - "venv/lib/python3.13/site-packages/git/refs/tag.py": 9979537867501757110, - "venv/lib/python3.13/site-packages/charset_normalizer/md.cpython-313-x86_64-linux-gnu.so": 1835299664481804968, - "docs/api/html/group__test__validate__path__creates__directory.html": 7116834460076222248, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/style.py": 10665998898253460114, - "venv/lib/python3.13/site-packages/passlib/handlers/scram.py": 12281469744083867450, - "docs/api/html/group__CleanReleaseCompliancePlugin.html": 18416685483683975434, - "backend/src/api/routes/translate/_dictionary_routes.py": 5668671879929670471, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_pbkdf2.py": 17016810698578473773, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_dialect.py": 6294522482440261756, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_set_name.py": 9094608008538024609, - "venv/lib/python3.13/site-packages/pip/_internal/cli/base_command.py": 10660882478126415399, - "backend/git_repos/remote/test-repo.git/hooks/pre-receive.sample": 6118492409684431911, - "docs/api/html/group__CandidateStatus.html": 1355862946696425686, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/requests.py": 14847842002253212098, - "venv/lib/python3.13/site-packages/jose/backends/_asn1.py": 14123487576864105793, - "docs/api/html/group__DatasetReviewSession.html": 6474466834876618927, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_has_cy.py": 11979732417254560424, - "venv/lib/python3.13/site-packages/pycparser/_ast_gen.py": 5464347629908906775, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/jupyter.py": 4446535289156468239, - "venv/lib/python3.13/site-packages/pydantic/validators.py": 8292819706488319845, - "backend/git_repos/remote/test-repo.git/objects/a7/49f11fad695dff77faf1708c7320867d0be1df": 4641609158325033031, - "docs/api/html/group__save__config.html": 12986586572590781292, - "docs/api/html/group__import__artifacts.html": 3620849422629917960, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.cpython-313-x86_64-linux-gnu.so": 15960606468596245469, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/WHEEL": 7795541085444298627, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_array_to_datetime.py": 3467480435568672840, - "docs/api/html/group__SemanticLayerReview.html": 10939641694027258903, - "venv/lib/python3.13/site-packages/cffi/_cffi_include.h": 13094899814611950155, - "venv/lib/python3.13/site-packages/PIL/ImImagePlugin.py": 15452956038794399492, - "venv/lib/python3.13/site-packages/pygments/lexers/wgsl.py": 9718384325049172855, - "docs/api/html/navtreeindex24.js": 8251711078163677251, - "docs/api/html/group__validate__path.html": 9061978304641306658, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-1.csv": 2202360898679162333, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/base.py": 17992335652981439676, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_take.py": 9847808489301498487, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/util.py": 2176177435997659667, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/pager.py": 18072372651585577904, - "frontend/src/routes/translate/+page.svelte": 15284819686967093613, - "frontend/build/_app/immutable/nodes/4.Bm444SfC.js": 15753381425875800765, - "docs/api/html/group__init__repository.html": 16356863681930678181, - "docs/api/html/group__sanitize__imported__filter__for__assistant.html": 11096067160073736807, - "venv/lib/python3.13/site-packages/click/_textwrap.py": 14586282603215052310, - "docs/api/html/group__test__endpoints__require__authentication.html": 6923064043031748125, - "docs/api/html/group__SupersetParsedContext.html": 16475087038796278202, - "docs/api/html/group__test__create__missing__name.html": 13297392854389513523, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_timezones.py": 4771674179164304731, - "docs/api/html/globals_u.html": 9479390597620491497, - "venv/lib/python3.13/site-packages/urllib3/filepost.py": 3814725067598042920, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_to_xml.py": 8477770807916990981, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_shape_manipulation.pyi": 3533739482029983817, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh15035.f": 13126319439858352711, - "docs/api/html/group__PreviewRow.html": 89719087155129857, - "venv/lib/python3.13/site-packages/cryptography/x509/base.py": 9124948989225782039, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_setitem.py": 14920902525192671548, - "backend/git_repos/remote/test-repo.git/objects/ee/59553241ef084b83d3351ba4c33caf2ed9a840": 11587106407031972706, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/encoder.py": 3261739474564948330, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/RECORD": 9461427699726982537, - "venv/lib/python3.13/site-packages/starlette/endpoints.py": 3379755567024530053, - "venv/lib/python3.13/site-packages/authlib/oauth1/errors.py": 1154126353335030046, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/mypy.py": 823265492261272631, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/histograms.pyi": 2281259341486424755, - "backend/src/api/routes/translate/_metrics_routes.py": 14203374652620439805, - "docs/api/html/group__delete__run.html": 15832347541244359990, - "venv/lib/python3.13/site-packages/passlib/handlers/bcrypt.py": 8125664010268654055, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/endpoint.py": 18260428408051728802, - "docs/api/html/group__adminService__Module.html": 6173971485001272907, - "venv/lib/python3.13/site-packages/websockets/client.py": 5229500079651465772, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_twodim_base.py": 17623133613018058380, - "docs/api/html/group____normalize__timestamp__value.html": 16608102179483469578, - "docs/api/html/group____latest__decision__for__candidate.html": 17368372638299477528, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/__init__.py": 293004106962245584, - "venv/lib/python3.13/site-packages/numpy/__init__.pyi": 15820654525292895396, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.pyi": 3595910367556664837, - "docs/api/html/group__WebSocketLogHandler.html": 930622065954511642, - "venv/lib/python3.13/site-packages/passlib/utils/decor.py": 21351391767102899, - "docs/api/html/group__TestReportAuditImmutability.html": 17837908356806354015, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_indexing.py": 14552860427542050771, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_cat_accessor.py": 4304062895283374927, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_header.py": 3564391095263545879, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_row.py": 918740441973901148, - "venv/lib/python3.13/site-packages/PIL/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/inferno.py": 9056274496469409594, - "docs/api/html/group__TestCoreScheduler.html": 8832629785112398533, - "venv/lib/python3.13/site-packages/pip/_internal/utils/compatibility_tags.py": 15862649917149158291, - "backend/alembic/versions/b0c1d2e3f4a5_cascade_ondelete_translate_fks.py": 1764442834210967136, - "docs/api/html/group__downloadFile__Function.html": 6136195968307907091, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/exceptions.py": 11303331612406662162, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo90.f90": 15981076495193915107, - "docs/api/html/group__TestDictionaryImport.html": 7122493165124405852, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE": 4672913476475760762, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/__init__.py": 15340047930888693984, - "venv/lib/python3.13/site-packages/pandas/core/groupby/base.py": 16888506204910366302, - "docs/api/html/group__create__dictionary.html": 15927500895860773676, - "venv/lib/python3.13/site-packages/pygments/lexers/int_fiction.py": 8806961041808606055, - "docs/api/html/group__derive__readiness__state.html": 15658475287495832903, - "run_clean_tui.sh": 14305120550349644601, - "docs/api/html/group____build__mock__stdscr.html": 18188773364932506767, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__main__.py": 111505188798650285, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_parquet.py": 11050150015772440923, - "docs/api/html/group__ResourceSourceRegistry.html": 11897883287885038128, - "docs/api/html/group__assistant__first__message__tests__Function.html": 1507496403746675511, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/connection.py": 15405047955974108860, - "venv/lib/python3.13/site-packages/gitdb/test/test_stream.py": 18159319442241857857, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/assertion_client.py": 4261295109880943573, - "venv/lib/python3.13/site-packages/numpy/random/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__extract__effective__filter__value.html": 5687710115716327783, - "venv/lib/python3.13/site-packages/authlib/oauth2/__init__.py": 13682931764896414722, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/models.py": 15928924478695638423, - "venv/lib/python3.13/site-packages/pydantic/version.py": 3699343596421001359, - "docs/api/html/group__requestDatasetReviewApi__Function.html": 8663890311322961011, - "venv/lib/python3.13/site-packages/pandas/_libs/groupby.pyi": 6154952803757623626, - "venv/lib/python3.13/site-packages/pandas/io/formats/excel.py": 797619981868084011, - "docs/api/html/group__ConfigManager.html": 2939116672940219717, - "venv/lib/python3.13/site-packages/jose/jws.py": 11592462993417182096, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pyodbc.py": 8175468565413744516, - "docs/api/html/group__index__by__uuid.html": 3464688372161598541, - "venv/lib/python3.13/site-packages/anyio/_core/_sockets.py": 10525237827959090532, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_hour.py": 13754646634123368797, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/util.py": 4815334059917765852, - "docs/api/html/group__MigrationArchiveParserModule.html": 245299514416957043, - "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.pyi": 3814281717779245045, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_legacy.py": 9226692984248279094, - "frontend/src/lib/components/dataset-review/__tests__/us3_execution_batch.ux.test.js": 17821671753127603158, - "backend/src/plugins/translate/_llm_http.py": 15686826696693103176, - "backend/src/api/routes/validation.py": 13836068409484617547, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.py": 1341849861184131135, - "venv/lib/python3.13/site-packages/numpy/lib/scimath.pyi": 18165025261847276375, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_records.py": 12217508428903265797, - "venv/lib/python3.13/site-packages/numpy/f2py/__version__.py": 8588600299261398598, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/array.py": 8065856342131357421, - "docs/api/html/group__resolveRepositoryStatusToken__Function.html": 17579339820007621981, - "frontend/build/_app/immutable/chunks/Ct73Sust.js": 3385889491616825090, - "docs/api/html/group__get__report__detail.html": 11469102499923216293, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py": 3515239440812977025, - "docs/api/html/group__DictionaryDetailPage.html": 4983283205838955822, - "venv/lib/python3.13/site-packages/requests/status_codes.py": 17005213506461710202, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/layout.py": 11554376181083053156, - "docs/api/html/group__test__banner__not__found.html": 8635777298336273949, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.cpython-313-x86_64-linux-gnu.so": 9014806524727977333, - "docs/api/html/group__ValidationApi.html": 8585641606625752685, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_downcast.py": 10399746645741452200, - "docs/api/html/group__update__task.html": 2054851862802669708, - "venv/lib/python3.13/site-packages/pygments/lexers/_openedge_builtins.py": 4979988747924637739, - "venv/lib/python3.13/site-packages/PIL/ImageQt.py": 6607270889359138659, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_show_versions.py": 10577134135855180356, - "backend/src/scripts/create_admin.py": 12963729348700590154, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nth.py": 17151595277666975085, - "docs/api/html/group____update__semantic__field__state.html": 9761433395553999447, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_astype.py": 7542068949396266883, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ctypeslib.pyi": 8623725566434725526, - "venv/lib/python3.13/site-packages/_pytest/faulthandler.py": 13329320306353718649, - "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.py": 503331864022631297, - "venv/lib/python3.13/site-packages/passlib/ext/django/models.py": 12386223748379294996, - "docs/api/html/group__ValidationRunListResponse.html": 1652982914005412268, - "venv/lib/python3.13/site-packages/pandas/io/excel/_odfreader.py": 2760916559416783928, - "venv/lib/python3.13/site-packages/pygments/lexers/smalltalk.py": 6113431347120223592, - "docs/api/html/search/functions_1.js": 11481182983342275162, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_extension_array_equal.py": 16119106336497630796, - "venv/lib/python3.13/site-packages/packaging/metadata.py": 9168255069163141470, - "backend/src/plugins/llm_analysis/__tests__/test_service.py": 17317408415318686082, - "venv/lib/python3.13/site-packages/passlib/handlers/misc.py": 18255658377109333797, - "docs/api/html/group__AuthSessionLocal.html": 12226538716122831966, - "venv/lib/python3.13/site-packages/pydantic/v1/networks.py": 13262073155118055511, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/sql.py": 13315906751371033170, - "backend/src/core/__tests__/test_native_filters.py": 3247249287628984886, - "venv/lib/python3.13/site-packages/pandas/core/sample.py": 11913688303879867963, - "venv/lib/python3.13/site-packages/jsonschema/_typing.py": 13152346021901861822, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_reductions.py": 14432782234851468060, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_cov_corr.py": 8117511855230249212, - "docs/api/html/dynsections.js": 9903691534904054073, - "docs/api/html/group__DatasetReviewFilterModels.html": 3609237248730765067, - "venv/lib/python3.13/site-packages/PIL/BufrStubImagePlugin.py": 5520941821175895889, - "venv/lib/python3.13/site-packages/pygments/lexers/func.py": 12257919753150082714, - "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js": 15614617116241362179, - "backend/src/services/reports/type_profiles.py": 9186322444310801411, - "backend/alembic/versions/aa1b2c3d4e5f_drop_deprecated_translate_columns.py": 15433357059529111004, - "docs/api/html/globals_func_v.html": 13758024740377138009, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_utils.py": 8668650648802195016, - "docs/api/html/group__test__concurrent__run__skips.html": 8827273582130280115, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/cookies.py": 4997226307650350876, - "docs/api/html/group____load__module.html": 9106125961389789525, - "docs/api/html/group__list__api__keys.html": 13338086264861974569, - "docs/api/html/group__BusinessSummarySource.html": 1094425953916303548, - "venv/lib/python3.13/site-packages/websockets/sync/messages.py": 15699091325918681038, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_extint128.py": 11074657869101074458, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/util.py": 955993052529739164, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/__init__.py": 17194870661869873083, - "backend/src/services/clean_release/source_isolation.py": 9041802363608399236, - "docs/api/html/group__test__get__dashboards__pagination__oversize.html": 18020532049469957534, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py": 9931394693284776391, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py": 10455718110956856820, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.pyi": 13110894861187422750, - "docs/api/html/group__test__reports__list__openapi__required__keys.html": 16840541688018374528, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE": 3996324383113221529, - "docs/api/html/group____task.html": 14186673195894587117, - "frontend/build/_app/immutable/chunks/DSxK3NUv.js": 8639953729041066468, - "frontend/src/lib/ui/HelpTooltip.svelte": 15082693035830243831, - "venv/lib/python3.13/site-packages/requests/help.py": 1885413874621672882, - "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiGet.html": 14001607880380347297, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/size/foo.f90": 18067977329435720684, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/RECORD": 3283324710045856319, - "docs/api/html/group____make__session.html": 3765523648902768678, - "venv/lib/python3.13/site-packages/jsonschema/validators.py": 3832874295500213309, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/AB.inc": 10441778083831997423, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_array.f90": 9711940829820746513, - "docs/api/html/group____require__fernet__key.html": 5961515902811999476, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_contains.py": 5273759677032634868, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/kbkdf.py": 1359143503575245224, - "venv/lib/python3.13/site-packages/PIL/MicImagePlugin.py": 13540523615822507963, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.pyi": 12925006572914814268, - "docs/api/html/group__DashboardsResponse.html": 9460608762964369730, - "venv/lib/python3.13/site-packages/typing_extensions.py": 8402965285733247527, - "backend/tests/services/clean_release/test_approval_service.py": 8273158625698254525, - "docs/api/html/group__BranchSchema.html": 986826748050343530, - "venv/lib/python3.13/site-packages/greenlet/tests/__init__.py": 1181423074160870921, - "docs/api/html/group__test__superset__client__import__dashboard__guard.html": 2274917772442192787, - "docs/api/html/group__SidebarTest__Module.html": 17956644577281325892, - "venv/lib/python3.13/site-packages/tzlocal/unix.py": 995019116408448073, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_multiarray.py": 3592197154340095431, - "venv/lib/python3.13/site-packages/pandas/tests/frame/conftest.py": 15090201792072045503, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/licenses/LICENSE": 11199044866758471950, - "docs/api/html/group__SupersetClientResolveTargetIdForDelete.html": 16317999718630461982, - "venv/lib/python3.13/site-packages/keyring/compat/__init__.py": 7578020229796085753, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/provision.py": 9472538482404392519, - "venv/lib/python3.13/site-packages/pygments/lexers/_qlik_builtins.py": 1728058904137960444, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/array.py": 7437107031079833232, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE": 17284252174641192866, - "docs/api/html/group__get__report.html": 3004053766336257006, - "frontend/src/routes/settings/LlmSettings.svelte": 10881646244972773692, - "docs/api/html/group__SupersetProfileLookup.html": 7471338261277310068, - "docs/api/html/group____get__repository.html": 857041345230199829, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_halfyear.py": 587199217232946404, - "venv/lib/python3.13/site-packages/rsa/pkcs1.py": 2576732594642491910, - "frontend/e2e/fixtures/auth.fixture.js": 13628564118439575126, - "backend/git_repos/remote/test-repo.git/objects/48/c3fde4d5fff5bf98e9bcaa5f328b199aa18292": 6121250272621110311, - "venv/lib/python3.13/site-packages/pygments/lexers/rdf.py": 14488319292387226505, - "docs/api/html/group__EncryptionKeyModule.html": 10145923645674172016, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/__init__.py": 7526309124863950484, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/exceptions.pyi": 9294219058621261971, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_logical_ops.py": 16166386974005224415, - "venv/bin/pyrsa-decrypt": 10929163076272054227, - "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so": 13917011878259810696, - "venv/lib/python3.13/site-packages/jaraco/classes/meta.py": 15930255620248873425, - "frontend/src/routes/settings/SystemSettings.svelte": 17084926955388049976, - "docs/api/html/group__AsyncAPIClient______init____.html": 2185805261011326243, - "frontend/build/_app/immutable/chunks/B00uzF5h.js": 14631562204103256604, - "docs/api/html/group__test__start__missing__tables.html": 12776370102130311528, - "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.pyi": 2042169688753984918, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/einsumfunc.pyi": 200771351328522379, - "docs/api/html/group__TaskService.html": 12725768316127349890, - "docs/api/html/group__render__prompt.html": 12771717951488483090, - "frontend/src/routes/datasets/__tests__/DatasetList.test.js": 10189122239208504978, - "venv/lib/python3.13/site-packages/pycparser/yacctab.py": 18018314512916353707, - "docs/api/html/group__TranslateMetricsRoutesModule.html": 13065795937255941504, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_label_or_level_utils.py": 3423644585913150841, - "venv/lib/python3.13/site-packages/git/diff.py": 6691274015605451565, - "docs/api/html/group__frontend__src__services__adminService__js____createADGroupMapping.html": 543017303917156388, - "docs/api/html/group__TuiFacadeAdapter.html": 9778651468218666889, - "docs/api/html/group__PublishRequest.html": 15758562664567551847, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate.py": 16723865897349422996, - "docs/api/html/group__update__dictionary.html": 9189704466838089002, - "venv/lib/python3.13/site-packages/PIL/WebPImagePlugin.py": 5378349556563611996, - "backend/src/api/routes/git_schemas.py": 10492336192775355750, - "venv/lib/python3.13/site-packages/more_itertools/__init__.py": 9858612105173082060, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_duplicates.py": 9830936017197354309, - "docs/api/html/group__ProfilePreferenceResponse.html": 14962554366043501331, - "venv/lib/python3.13/site-packages/cffi/_imp_emulation.py": 17246725564704952212, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpq-9b38f5e3.so.5.17": 9680474575710585826, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_setops.py": 17350958906336246626, - "docs/api/html/group__test__json__load__if__needed__empty__string.html": 4809024742473574705, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/WHEEL": 2357997949040430835, - "docs/api/html/group__test__editRow__Function.html": 7862387474531942746, - "venv/lib/python3.13/site-packages/idna/core.py": 7725813731640753267, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_append.py": 17016125916761365423, - "venv/lib/python3.13/site-packages/PIL/GribStubImagePlugin.py": 14954946414134984896, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_random.py": 4179188367630060654, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/METADATA": 3474626408138844944, - "backend/git_repos/remote/test-repo.git/objects/22/c0ea49cea7e5138fb91b25b1bc0e101ae6e017": 17630871400599334193, - "docs/api/html/group__get__task__log__level.html": 8101832559719739013, - "docs/api/html/group__DeployRequest.html": 15085302046619241640, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_value_attrspec.py": 7741590618654448240, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq_py.py": 16054175754674285311, - "docs/api/html/group__test__task__logger__initialization.html": 11239153689675430218, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/hooks.py": 9888227370911151341, - "venv/lib/python3.13/site-packages/pandas/compat/_optional.py": 12714208019736797073, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/scoping.py": 18223258186431020473, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__SupersetClientGetDashboardsSummaryPage.html": 12787892430248800577, - "docs/api/html/group__DatasetPreview.html": 13745385565163960445, - "venv/lib/python3.13/site-packages/pandas/_testing/_warnings.py": 5640964566228306285, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py": 7301817934902711510, - "docs/api/html/group__test__provision__adfs__user__new.html": 1392791487923839022, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/WHEEL": 16223367184831500348, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py": 16550134594771291234, - "docs/api/html/group__ConsolidatedSettingsResponse.html": 6466679869565868829, - "venv/lib/python3.13/site-packages/starlette/middleware/errors.py": 1337979724602864440, - "venv/lib/python3.13/site-packages/numpy/core/einsumfunc.py": 3484247123731380377, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_numpy.py": 9825804096508014886, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_subclass.py": 10949229764696473939, - "docs/api/html/group____enforce__session__version.html": 8264349099371763057, - "docs/api/html/group__taskLogs__Data.html": 10064937378577441554, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/table.py": 16856253349556498073, - "merge_spec.py": 13855568343782022, - "venv/lib/python3.13/site-packages/numpy/_core/getlimits.py": 7842717073693731093, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress_bar.py": 15595912766961891283, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/RECORD": 12015288851076448205, - "docs/api/html/group__AssistantRoutes.html": 11177343112252329246, - "docs/api/html/group__test__us2__field__semantic__override__lock__unlock__and__feedback.html": 7173438708880743570, - "docs/api/html/group__CheckStageName.html": 3993716435006752710, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_chained_assignment_deprecation.py": 7694137945143129474, - "backend/src/api/routes/dashboards/_helpers.py": 13624740316332136004, - "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py": 5075014721013543720, - "venv/lib/python3.13/site-packages/pygments/formatters/rtf.py": 5850574956253747036, - "docs/api/html/group__DatasetMapperModule.html": 15394484234769389552, - "docs/api/html/group__ValidationRunDetailResponse.html": 2243954401431072205, - "docs/api/html/group__prepare__candidate__legacy.html": 17583352627738261078, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/REQUESTED": 15130871412783076140, - "backend/src/schemas/dataset_review.py": 7490971410706145392, - "venv/lib/python3.13/site-packages/keyring/backends/chainer.py": 12526770797516553385, - "venv/lib/python3.13/site-packages/pandas/io/api.py": 8275376846208437741, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply_relabeling.py": 5600949862632014700, - "docs/api/html/group__TestValidatePath.html": 12521153993368156395, - "venv/lib/python3.13/site-packages/pandas/tests/test_expressions.py": 16378925536309828288, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__belief__scope.html": 2116670201736502743, - "backend/src/plugins/git/llm_extension.py": 5713196889309291531, - "backend/src/api/routes/__tests__/test_clean_release_v2_api.py": 17447140422807500877, - "docs/api/html/group__ProfilePreferenceService.html": 9611968709231329554, - "venv/lib/python3.13/site-packages/PIL/FtexImagePlugin.py": 8196606399875185931, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.c": 9318655518998644962, - "venv/lib/python3.13/site-packages/numpy/random/_generator.pyi": 17167649524261942483, - "venv/lib/python3.13/site-packages/pandas/io/formats/css.py": 11288129826285529457, - "docs/api/html/group__MaintenanceDashboardScanner.html": 7477142727721457580, - "docs/api/html/group__Footer.html": 12554974943803824206, - "venv/lib/python3.13/site-packages/pandas/compat/numpy/__init__.py": 1037113402100731082, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/validator_creation.py": 2745563768861102999, - "venv/lib/python3.13/site-packages/starlette/authentication.py": 10905269573809129547, - "venv/lib/python3.13/site-packages/pygments/lexers/asn1.py": 15030988816882821134, - "backend/git_repos/remote/test-repo.git/hooks/pre-commit.sample": 102291230237676317, - "docs/api/html/group__test__get__dashboard__detail__success.html": 2526597651975031803, - "venv/lib/python3.13/site-packages/numpy/dtypes.pyi": 6713822658171717369, - "venv/lib/python3.13/site-packages/referencing/tests/test_core.py": 8280983300775878144, - "docs/api/html/group__DashboardHub__toggleFilterValue__Function.html": 4775698047438821697, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_interval.py": 14749388246573356959, - "venv/lib/python3.13/site-packages/pygments/lexers/_lua_builtins.py": 12653747099659257203, - "docs/api/html/group__TranslateJobList.html": 13432818086805315799, - "docs/api/html/group__test__auth__config__module__contract.html": 10228088760530679113, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/h11/py.typed": 12358286306858197118, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/attributes.py": 5068814626384132900, - "venv/lib/python3.13/site-packages/pygments/styles/onedark.py": 17580335614588531275, - "venv/lib/python3.13/site-packages/pandas/core/tools/numeric.py": 8510988912136285022, - "venv/lib/python3.13/site-packages/pillow.libs/libsharpyuv-95d8a097.so.0.1.2": 5320647379976541240, - "venv/lib/python3.13/site-packages/itsdangerous/url_safe.py": 13454753900702274266, - "docs/api/html/group__test__persist__task__failed__extracts__error.html": 3464011014621751693, - "venv/lib/python3.13/site-packages/pydantic/fields.py": 4142211199816898047, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reshaping.py": 18085648077811502689, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_numba.py": 1668304729617150075, - "frontend/src/routes/validation/+page.svelte": 9845194549295283559, - "venv/lib/python3.13/site-packages/pygments/styles/zenburn.py": 1031869867933871309, - "venv/lib/python3.13/site-packages/numpy/random/__init__.pyi": 13833571330152615376, - "frontend/src/routes/settings/__tests__/settings_page.ux.test.js": 14507112961464496362, - "docs/api/html/group__buildAssistantSeedMessage__Function.html": 8962217409153936203, - "docs/api/html/group__EnvironmentResponse.html": 9633575500379521671, - "venv/lib/python3.13/site-packages/pillow.libs/liblzma-61b1002e.so.5.8.2": 11405063484938458842, - "venv/lib/python3.13/site-packages/pandas/io/excel/_calamine.py": 13354427767660487637, - "backend/src/core/utils/dataset_mapper.py": 3978140070131451501, - "docs/api/html/group__test__extra__form__data__merge.html": 8128733170632705704, - "docs/api/html/group__confirm__operation.html": 3794428125492004515, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.cpython-313-x86_64-linux-gnu.so": 657138415665704488, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_store.py": 1500300322083708794, - "venv/lib/python3.13/site-packages/pygments/lexers/chapel.py": 7619603166448145621, - "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.cpython-313-x86_64-linux-gnu.so": 9380059465932560412, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_common.py": 2265736743367879051, - "frontend/static/favicon.png": 16740551781088792605, - "venv/lib/python3.13/site-packages/git/refs/log.py": 5175801243332619268, - "backend/git_repos/remote/test-repo.git/objects/ed/0432f88f5f7d95b26ecde34345abe741dd9db2": 13426916511127204463, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libssl-81ffa89e.so.3": 13913673119166658781, - "docs/api/html/group__TestLayoutUtils.html": 3035905040939536258, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi": 11455144434558323236, - "venv/lib/python3.13/site-packages/pydantic/v1/config.py": 12050640083719340958, - "venv/lib/python3.13/site-packages/pandas/tests/config/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_rank.py": 5398520257852104723, - "docs/api/html/group__SupersetCompilationAdapter____supports__client__method.html": 15057319214542689475, - "venv/lib/python3.13/site-packages/PIL/_binary.py": 14039167720582222043, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py": 13454394827268856575, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/util.py": 9373891872370603585, - "docs/api/html/group__fetchTask__Function.html": 3934212245276248677, - "docs/api/html/group__test__get__detector__cache.html": 7969731651409649319, - "docs/api/html/group____collect__yaml__objects.html": 706456919847215611, - "docs/api/html/group____json__safe__value.html": 1390575450608766153, - "venv/lib/python3.13/site-packages/uvicorn/main.py": 5843333502356171120, - "venv/lib/python3.13/site-packages/_pytest/assertion/truncate.py": 14000392306994773741, - "frontend/build/_app/immutable/nodes/1.Cyet0ba6.js": 12906236210713562147, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/METADATA": 8128818962678047497, - "docs/api/html/group__ReportsPageTest__Module.html": 7688339467374188309, - "venv/lib/python3.13/site-packages/pandas/core/window/__init__.py": 15180240812077877933, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py": 3042005457138956499, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_datetime.py": 7862725028865965163, - "docs/api/html/group__test__abort__merge__calls__service__and__returns__result.html": 12531578259821930099, - "frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js": 16890149311572703440, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_modules.py": 16835987029123241300, - "docs/api/html/group__TargetSchemaValidationResponse.html": 4850993964475160143, - "docs/api/html/group__LLMTranslationService.html": 12719944626760388569, - "venv/lib/python3.13/site-packages/PIL/ImagePalette.py": 9227837918026991107, - "venv/lib/python3.13/site-packages/numpy/tests/test_ctypeslib.py": 9592016432737074765, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/installed.py": 18153165899841229597, - "venv/lib/python3.13/site-packages/pygments/lexers/scripting.py": 4167168074165473790, - "venv/lib/python3.13/site-packages/requests/cookies.py": 4997226307650350876, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_timer.py": 8196003526646080498, - "venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py": 3893922370396791826, - "venv/lib/python3.13/site-packages/_pytest/main.py": 931018496411984909, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_arithmetics.py": 16527169124402715210, - "venv/lib/python3.13/site-packages/pandas/io/__init__.py": 9767690163638898709, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_round.py": 14996007456708990295, - "venv/lib/python3.13/site-packages/psycopg2/sql.py": 18041240927130590808, - "docs/api/html/group__TranslationBatchResponse.html": 15097744887458440847, - "venv/lib/python3.13/site-packages/pandas/tests/io/__init__.py": 15130871412783076140, - "docs/api/html/group__TranslateApi.html": 7635708107203092858, - "docs/api/html/group____recover__truncated__rows.html": 4179907453973361909, - "docs/api/html/globals_w.html": 15465521333184498068, - "docs/api/html/group__test__rejectRow__Function.html": 14197518524874918963, - "docs/api/html/group__test__api__create__job.html": 15555049302714485159, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_cpu.h": 13799366908726531089, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.pyi": 14543653817631274394, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/core/tools/datetimes.py": 13313374952742129866, - "venv/lib/python3.13/site-packages/more_itertools/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/click/_utils.py": 16491408631876616271, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_all_methods.py": 18227706645906325527, - "docs/api/html/group__MappingService.html": 14790116697814336524, - "venv/lib/python3.13/site-packages/typing_inspection/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/httpx/_client.py": 1063593889704982038, - "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.pyi": 8708830448398500675, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/identity.py": 326059526167697655, - "venv/lib/python3.13/site-packages/urllib3/util/ssltransport.py": 1175688460659745788, - "venv/lib/python3.13/site-packages/pygments/lexers/python.py": 8995519134978505744, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/nit.py": 18262907536841158657, - "docs/api/html/group__StorageConfig.html": 12738778311037071479, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.pyi": 2700568594438441246, - "docs/api/html/group__HealthSchemas.html": 5884366198523374362, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/packaging/_manylinux.py": 8268418900652790584, - "venv/lib/python3.13/site-packages/jeepney/io/common.py": 15160647397460834508, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/modules.pyi": 14176187662591164602, - "frontend/src/lib/components/layout/__tests__/test_topNavbar.svelte.js": 1873684170678703500, - "frontend/src/routes/validation/[id]/+page.svelte": 4125480744270905746, - "venv/lib/python3.13/site-packages/pygments/lexers/_php_builtins.py": 2344562378679281201, - "backend/test_app.db": 2486636812009447051, - "docs/api/html/group__get__native__filter__state__async.html": 204597673029712023, - "docs/api/html/group__SupersetClientGetDashboard.html": 5538675640686031469, - "docs/api/html/group__isDirectory__Function.html": 3452622402682011690, - "venv/lib/python3.13/site-packages/jsonschema/tests/typing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/rec/__init__.pyi": 1782063765067823837, - "venv/bin/pyrsa-sign": 15564653706771700451, - "venv/lib/python3.13/site-packages/urllib3/_collections.py": 3440910792774742262, - "venv/lib/python3.13/site-packages/pygments/lexers/dsls.py": 11696359794961833087, - "backend/src/plugins/debug.py": 13683182174969777939, - "docs/api/html/search/all_12.js": 9323517322081325944, - "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.h": 5980733638838895780, - "venv/lib/python3.13/site-packages/pandas/_config/display.py": 433650013567734821, - "backend/src/core/task_manager/manager.py": 1504518197419779491, - "backend/src/plugins/translate/preview.py": 5579180663525515759, - "docs/api/html/search/groups_1.js": 5807190646834111663, - "venv/lib/python3.13/site-packages/numpy/tests/test__all__.py": 10689265249302359388, - "venv/lib/python3.13/site-packages/pygments/lexers/robotframework.py": 11507713622179102496, - "docs/api/html/group__cancel__Function.html": 843296188658809904, - "docs/api/html/search/all_f.js": 12596024952098859181, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval_new.py": 3893421355544517336, - "backend/git_repos/remote/test-repo.git/objects/ae/8d2f5dec5d421b2d6cc1eebaad6d2ef901a90d": 15877648001251681939, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/flatiter.pyi": 6885824496927786205, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_api.py": 6439927798101672423, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/toml.py": 2225382735669153136, - "docs/api/html/group__BASE__DIR.html": 4143991800839240540, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/__init__.py": 15130871412783076140, - "docs/api/html/group__LLMHttpClient.html": 14802937964955012182, - "docs/api/html/group__DashboardHealthItem.html": 14118707309412866355, - "venv/lib/python3.13/site-packages/greenlet/tests/test_weakref.py": 6780204527427742510, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/bitwise_ops.pyi": 751490187478960891, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_fillna.py": 7349777469648334282, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_internal/commands/lock.py": 14782882714168174465, - "docs/api/html/nav_h.png": 18087931637685379732, - "docs/api/html/group____normalize__base__url.html": 5091254209090535827, - "docs/api/html/group__sanitize__secret.html": 18277308754712559382, - "docs/api/html/group__GitServerConfigCreate.html": 6897428372780872078, - "docs/api/html/group__TestGitServiceGiteaPr.html": 5702873534344534880, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi": 16934250836612138541, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/methods.py": 8059372834301557565, - "docs/api/html/group____require__owner__mutation__scope.html": 370851885631546971, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/__init__.py": 9820770319106921495, - "docs/api/html/group____get__or__init__decisions__store.html": 15627912227755842908, - "frontend/playwright-report/trace/sw.bundle.js": 10879347794748701702, - "docs/api/html/group__test__update__job.html": 13489385965364167749, - "venv/lib/python3.13/site-packages/click/utils.py": 13475604420010022458, - "frontend/build/_app/immutable/chunks/DKXhHShG.js": 5818012674247396946, - "docs/api/html/group__mask__secret__value.html": 2433163108890976342, - "backend/src/plugins/translate/orchestrator_retry.py": 2531207787915962557, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dsa.py": 8079219938116542284, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi": 13850254283540522444, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_upcast.py": 6496794053681247105, - "backend/src/api/routes/__tests__/test_dataset_review_api.py": 9392418725327519178, - "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django_source.py": 15577951591316518967, - "docs/api/html/group__delete__chart.html": 6831863026577067657, - "frontend/src/lib/utils.js": 2362133402815996206, - "venv/lib/python3.13/site-packages/pygments/lexers/_tsql_builtins.py": 829193091321611630, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_frozen.py": 3484963987053884566, - "docs/api/html/group__handleTestEnv__Function.html": 9541743355280005945, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/rec.pyi": 9140655366667421698, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_list_accessor.py": 8613662386779664039, - "docs/api/html/group__test__get__task__logs__invalid__limit.html": 7729306372384942790, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/utils.py": 3710235316419562274, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_fileno.py": 15541504502174047167, - "docs/api/html/group__get__task__manager.html": 17905058662693391528, - "docs/api/html/group__truncate__text.html": 15964456244302237036, - "docs/api/html/group__StartSessionResult.html": 15571965797483331043, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/named_types.py": 6099185243355915896, - "venv/lib/python3.13/site-packages/sqlalchemy/log.py": 10101221585967121334, - "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.py": 15031197044862908440, - "venv/lib/python3.13/site-packages/pandas/_libs/sparse.cpython-313-x86_64-linux-gnu.so": 2051484129454294720, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_algorithms.py": 15219408648894515493, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_lexsort.py": 10394832796613968484, - "docs/api/html/group__compute__preview__fingerprint.html": 9160170929765714565, - "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.pyi": 9629457630163613604, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polyutils.py": 675639791972685455, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_join.py": 2585969863606902743, - "docs/api/html/group__downloadFileUrl__Function.html": 10917163696944210952, - "backend/src/models/__tests__/test_report_models.py": 1002634514476529590, - "venv/lib/python3.13/site-packages/httpcore/_async/socks_proxy.py": 9026788710898138090, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_compat.py": 6032546839876195115, - "docs/api/html/group__frontend__src__services__git__utils__js____buildSuggestedRepoName.html": 9912150894737317582, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py": 7947998260347730581, - "docs/api/html/group__test__auth.html": 2421668339643460366, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunclike.py": 12077553320554633586, - "docs/api/html/group__AuthPackage.html": 9983939503119143826, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_interval.py": 2911379732293827726, - "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.pyi": 10638891136586198429, - "venv/lib/python3.13/site-packages/PIL/_deprecate.py": 15076725072094847812, - "venv/lib/python3.13/site-packages/secretstorage/__init__.py": 4535964275649588672, - "docs/api/html/group__ComplianceExecutionService.html": 2017272940317881457, - "venv/lib/python3.13/site-packages/pandas/core/window/common.py": 17956497339875932377, - "frontend/src/lib/stores/__tests__/test_taskDrawer.js": 14849615218402542643, - "venv/lib/python3.13/site-packages/numpy/_globals.py": 8736093011628027941, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_blocking.py": 6312017389529916004, - "venv/lib/python3.13/site-packages/pandas/tests/series/__init__.py": 15130871412783076140, - "docs/api/html/group____serialize__session__summary.html": 12633044514321560047, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_subplots.py": 4596089143375088346, - "docs/api/html/group__validate__timezone.html": 10510695123016011574, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/__init__.py": 15130871412783076140, - "docs/api/html/group__test__content__with__padding.html": 3201553403801844016, - "docs/api/html/group__get__clean__release__facade.html": 9736682632079627043, - "venv/lib/python3.13/site-packages/fastapi/py.typed": 15130871412783076140, - "docs/api/html/group__GitServiceMergeMixin.html": 4354986681189316190, - "venv/lib/python3.13/site-packages/sqlalchemy/schema.py": 16029542421991709659, - "venv/lib/python3.13/site-packages/pandas/core/internals/construction.py": 893951849732478990, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/registry.py": 13306388361671753373, - "frontend/src/routes/profile/+page.svelte": 13080303244628934788, - "docs/api/html/group__AuthApi.html": 1701222343226214552, - "docs/api/html/group__GitPlugin.html": 16223539126912136426, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/exc.py": 3242780937124999391, - "venv/lib/python3.13/site-packages/websockets/sync/server.py": 7966978001578584901, - "venv/lib/python3.13/site-packages/numpy/core/_internal.py": 14816989375912210674, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/parameters.py": 12219404446503940066, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286_bc.pyf": 10893958728966010450, - "venv/lib/python3.13/site-packages/greenlet/tests/test_stack_saved.py": 7767924647054186572, - "docs/api/html/group__handleDeleteUser__Function.html": 7705917636267502529, - "docs/api/html/group__inline__edit__translation.html": 14461008762407918187, - "venv/lib/python3.13/site-packages/greenlet/greenlet.cpp": 3281107194043468270, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/endpoints.py": 18334891787808122725, - "docs/api/html/group__TraceContextMiddleware.html": 8346923034582577387, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/__init__.py": 11414161642984633362, - "venv/lib/python3.13/site-packages/pygments/lexers/j.py": 9766594937623918053, - "backend/git_repos/remote/test-repo.git/refs/heads/feature/e2e-update": 9577764946931187657, - "docs/api/html/group__test__tui__run__checks__f5.html": 11086893283846655051, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiprocessing.py": 5522490547285108448, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_utils.py": 11574890848475750509, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_na_values.py": 13527837495439739279, - "backend/src/services/__tests__/test_llm_prompt_templates.py": 12364821019604393930, - "docs/api/html/globals_func_g.html": 530559393513453799, - "backend/src/core/task_manager/models.py": 11636421249104836957, - "docs/api/html/group__fixtures.html": 7724348357498077937, - "docs/api/html/group__DashboardDetailRoutes.html": 8608175949358740288, - "docs/api/html/group__AsyncAPIClient__authenticate.html": 9150281512735244674, - "venv/lib/python3.13/site-packages/pygments/lexers/usd.py": 934820244673741695, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/events.py": 7576713358952064464, - "docs/api/html/group__LLMProviderConfig.html": 477820453193814610, - "docs/api/html/group__TestContext.html": 16549518825764564714, - "docs/api/html/group__DashboardHub__applyGridTransforms__Function.html": 6805737041086802084, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_head_tail.py": 1906121476322841736, - "docs/api/html/group__test__start__session__endpoint__returns__created__summary.html": 4072374425456734100, - "docs/api/html/group__AggregatedMetricsResponse.html": 5296336191776764582, - "docs/api/html/group__test__list__runs__filter__status.html": 7762165836370653178, - "venv/lib/python3.13/site-packages/numpy/random/lib/libnpyrandom.a": 1802449692091040396, - "docs/api/html/group__ResourceSourceEntry.html": 8011516002332864562, - "docs/api/html/group__SupersetClientValidateQueryParams.html": 1288505283032735283, - "docs/api/html/group__extract__dataset__id__from__form__data.html": 6984612851511739917, - "docs/api/html/group__test__get__datasets__success.html": 17085020636702913343, - "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.py": 738588649412338185, - "venv/lib/python3.13/site-packages/pandas/core/sparse/api.py": 9094651265692218821, - "docs/api/html/group__test__maintenance__api.html": 10206796814843197798, - "backend/git_repos/remote/test-repo.git/hooks/pre-merge-commit.sample": 10013699873067956556, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/LICENSE": 14599279238716790189, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.pyi": 15420142722808118826, - "docs/api/html/group__DictionaryImportExport.html": 4551906910630157436, - "venv/lib/python3.13/site-packages/pillow.libs/libwebp-d8b9687f.so.7.2.0": 116215608212382352, - "venv/lib/python3.13/site-packages/pandas/core/algorithms.py": 487939626203097713, - "venv/lib/python3.13/site-packages/pandas/_libs/testing.pyi": 16691791330088523786, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/state_changes.py": 11425629729736599320, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/use_data.f90": 14624074967635860696, - "backend/src/core/superset_profile_lookup.py": 7466572583388472449, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/utils.py": 2959395751217827103, - "backend/src/core/migration/risk_assessor.py": 4161499861941446814, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/cmac.py": 12011944740750051829, - "venv/lib/python3.13/site-packages/pillow.libs/libtiff-295fd75c.so.6.2.0": 17866578820357536656, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/nested_schemas.py": 4114645644496880787, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo.f": 5690596225005490325, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_names.py": 10037208029090713121, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_orc.py": 1248059610437937895, - "backend/tests/scripts/test_clean_release_tui.py": 17813178142456308266, - "docs/api/html/group__create__admin.html": 11166797148814690447, - "venv/lib/python3.13/site-packages/PIL/FitsImagePlugin.py": 618556384932056120, - "backend/src/api/routes/git/_repo_routes.py": 12158952761025397291, - "venv/lib/python3.13/site-packages/pip/_internal/utils/deprecation.py": 7435269308988385470, - "backend/src/core/mapping_service.py": 5863886462891122323, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_index.py": 14187428934500390688, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_chaining_and_caching.py": 11784574265874608859, - "docs/api/html/group__TestDashboardsApi.html": 5509739390736522040, - "docs/api/html/group__BreadcrumbsContractTest__Module.html": 11002777887735789654, - "venv/lib/python3.13/site-packages/pygments/styles/pastie.py": 15784474766258831882, - "backend/tests/test_sql_table_extractor.py": 13392885324292384588, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/__init__.py": 9106887608906281686, - "venv/lib/python3.13/site-packages/PIL/_webp.cpython-313-x86_64-linux-gnu.so": 11070012119632935103, - "docs/api/html/group__TranslationPreviewLanguage.html": 13625916657703783936, - "docs/api/html/group__validate__target__table__schema.html": 17860602982230273522, - "venv/lib/python3.13/site-packages/pandas/core/window/expanding.py": 14141093310397856286, - "docs/api/html/group__TraceContextMiddlewareModule.html": 16492855860524137184, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/__init__.py": 2436195108273216959, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/npyio.pyi": 14147712281086095231, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ma.py": 15912582628112164509, - "venv/lib/python3.13/site-packages/numpy/_core/memmap.py": 4093510602708416858, - "venv/lib/python3.13/site-packages/pandas/core/computation/pytables.py": 16326122536715961388, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraysetops.py": 10588903945244480732, - "frontend/playwright-report/trace/codicon.DCmgc-ay.ttf": 15579197054804464248, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunc_config.pyi": 2005481648919369399, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_construction.py": 10619501873883861745, - "venv/lib/python3.13/site-packages/click/__init__.py": 9261629899056870564, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/test_downstream.py": 5635510753354256507, - "venv/lib/python3.13/site-packages/pandas/io/pickle.py": 5418646169167774144, - "venv/lib/python3.13/site-packages/yaml/composer.py": 13619070314468518566, - "backend/src/services/__init__.py": 15123879863527585264, - "venv/lib/python3.13/site-packages/uvicorn/loops/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/__init__.py": 4390908730288159455, - "docs/api/html/group__PlaywrightConfig.html": 300679414561245473, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/unix.py": 13808187640638677050, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_categorical.py": 18254320571347851153, - "backend/src/models/config.py": 17311376253737985673, - "venv/lib/python3.13/site-packages/httpcore/_backends/__init__.py": 15130871412783076140, - "docs/api/html/group__TranslateCorrectionRoutesModule.html": 12198261775802784372, - "docs/api/html/group__UseGitManager.html": 11005313010951220315, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/__init__.py": 4014740126161973332, - "venv/lib/python3.13/site-packages/numpy/_typing/_char_codes.py": 14162745886150625150, - "docs/api/html/group__frontend__src__services__toolsService__js____runTask.html": 8322693038275083284, - "venv/lib/python3.13/site-packages/pydantic/v1/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/gitdb/const.py": 14122142034887286559, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py": 11808051681178127650, - "docs/api/html/group____normalize__actor__alias__token.html": 5763510838664670977, - "backend/src/services/git/_branch.py": 1746193863798413085, - "venv/lib/python3.13/site-packages/authlib/jose/errors.py": 15231759002942436632, - "docs/api/html/group__test__create__schedule.html": 6101710034110852291, - "venv/lib/python3.13/site-packages/pip/_internal/operations/freeze.py": 3469845570614985559, - "docs/api/html/group__DatabaseMapping.html": 6106397605843909678, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/testing.pyi": 16263777835572515739, - "docs/api/html/group__test__verify__password__empty__hash.html": 5792654688365761152, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiarray.py": 5928288171180750841, - "venv/lib/python3.13/site-packages/pandas/core/arrays/string_.py": 14865733321317939478, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/string.f": 17328443345813567449, - "frontend/build/_app/immutable/nodes/15.D75TwS7V.js": 13696333152806937873, - "venv/lib/python3.13/site-packages/pandas/core/window/doc.py": 4204367435727844696, - "docs/api/html/group__handleManageSelected__Function.html": 6277194528006873152, - "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js": 17118608261683412409, - "docs/api/html/group__GitMergeDialog.html": 16071754970378737774, - "docs/api/html/group__test__map__columns__empty__ids.html": 6731261812856257693, - "docs/api/html/group__SupersetClientGetChart.html": 12922958291009873472, - "docs/api/html/group__progress.html": 3466818030353762768, - "docs/api/html/group__approve__candidate.html": 16919945520220435820, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_.py": 644667973700652263, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_value_counts.py": 13270420637302622174, - "backend/src/models/dataset_review_pkg/_semantic_models.py": 6371476812115351221, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.pyx": 7085380878820747579, - "docs/api/html/group____trigger__validation.html": 7745124024594471315, - "docs/api/html/group__llm__provider.html": 10759065886221772665, - "frontend/src/routes/datasets/__tests__/inline_edit.test.js": 17712409018171053453, - "docs/api/html/group__clean__release__dto.html": 8303338663879364604, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/wrapper.py": 14599195010683170123, - "venv/lib/python3.13/site-packages/pandas/core/interchange/__init__.py": 15130871412783076140, - "backend/src/services/maintenance/_chart_manager.py": 11268757631496775895, - "docs/api/html/group__CandidateMatchType.html": 12481194623088347299, - "docs/api/html/group__handleSelect__Function.html": 16158838263000329677, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_subclassing.py": 17318983445227658412, - "frontend/build/_app/immutable/chunks/C20922N0.js": 14681700380768364777, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_compare.py": 171535659467902751, - "docs/api/html/group__update__language__stats.html": 15508043268515320115, - "docs/api/html/group__startDataTimeout__Function.html": 93996475289484245, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/__init__.py": 15130871412783076140, - "docs/api/html/group__test__api__update__job__not__found.html": 17098826918877275745, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_indexing.py": 7181087459615188551, - "frontend/e2e/run-enterprise-clean-e2e.sh": 11626901202531925046, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/__init__.py": 17454177076687312424, - "venv/lib/python3.13/site-packages/requests/certs.py": 16052880877156864882, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/_mapping.py": 13335227913146542481, - "venv/lib/python3.13/site-packages/pygments/styles/sas.py": 12363791134941577382, - "venv/lib/python3.13/site-packages/passlib/ext/django/__init__.py": 11713862539944289631, - "venv/lib/python3.13/site-packages/rsa/util.py": 13423880475585138638, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/nonce.py": 18219168479887197733, - "docs/api/html/group__ValidationTaskResponse.html": 10449136656682527650, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numerictypes.pyi": 15443101937711720707, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_conversion_utils.py": 1567199588622767277, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/conftest.py": 6137462119729731945, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_shift.py": 14681514853406253657, - "docs/api/html/group__ApiRoutesGetAttr.html": 10833820970838947139, - "docs/api/html/search/functions_13.js": 8491110692914206388, - "venv/lib/python3.13/site-packages/requests/utils.py": 17045260098091371478, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit.py": 15498810174380798405, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/comparisons.pyi": 4158883009780474220, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_indexing.py": 16623126293364224089, - "frontend/build/_app/immutable/nodes/14.D6hALL1X.js": 8372887877621592663, - "docs/api/html/group__test__git__service__configure__identity__updates__repo__local__config.html": 5513064374674137589, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_overrides.py": 92685611709898742, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_formats.py": 3525283725314253763, - "frontend/src/routes/reports/+page.svelte": 18404964858248487921, - "docs/api/html/group__formatDate__Function.html": 15997975478657062019, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/__init__.py": 15130871412783076140, - "docs/api/html/group__test__component__exists.html": 8771500170212558619, - "docs/api/html/group__BulkReplaceModal.html": 1782439446991758677, - "venv/lib/python3.13/site-packages/httpcore/_async/http2.py": 16350817785499681224, - "backend/tests/test_log_persistence.py": 7807469395396702613, - "venv/lib/python3.13/site-packages/pandas/core/_numba/executor.py": 2205130934861422769, - "backend/src/services/clean_release/stages/data_purity.py": 7015962973560119808, - "docs/api/html/group__get__task__log__stats.html": 13399644287740867608, - "venv/lib/python3.13/site-packages/_pytest/subtests.py": 14573790885130724739, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_asyncio.py": 2820168745710754136, - "docs/api/html/group__DashboardTaskHistoryResponse.html": 4494151273240950242, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_str_accessor.py": 4066094997616994612, - "frontend/build/_app/immutable/chunks/BE93hbNr.js": 10367577961995972982, - "frontend/src/lib/components/translate/ScheduleConfig.svelte": 9548866160759602637, - "venv/lib/python3.13/site-packages/anyio/_core/_streams.py": 6436461519284884601, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/METADATA": 14180973198574731599, - "docs/api/html/group__resolve__merge__conflicts.html": 237817549844254377, - "venv/lib/python3.13/site-packages/PIL/WalImageFile.py": 12442826195371137238, - "backend/src/core/encryption.py": 6401049364294007361, - "docs/api/html/search/all_2.js": 12325006021233833726, - "docs/api/html/group__test__constants__are__immutable.html": 18426400293933334698, - "docs/api/html/group__DictionaryResolutionResult.html": 4103555385968464679, - "docs/api/html/group__compute__config__hash.html": 6669149726626922468, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/__init__.py": 4770885427721822507, - "venv/lib/python3.13/site-packages/python_multipart/exceptions.py": 5971058243218049183, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_types.py": 7175518113263603341, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_truncate.py": 6478126371906156409, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/status_codes.py": 17005213506461710202, - "venv/lib/python3.13/site-packages/pygments/lexers/tact.py": 1164971357219795420, - "backend/src/services/clean_release/mappers.py": 16365217670331724280, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel.py": 7728646523045538484, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/datonly.f90": 10495925225338237742, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_partial_slicing.py": 15567512240469921599, - "docs/api/html/globals_func_s.html": 12786975089129434154, - "docs/api/html/group__cot__logger__instance.html": 13706182001628895155, - "venv/lib/python3.13/site-packages/dateutil/parser/_parser.py": 9443336890192151397, - "venv/lib/python3.13/site-packages/fastapi/param_functions.py": 14585101566697773072, - "frontend/src/lib/components/assistant/AssistantChatPanel.svelte": 17490466635940795553, - "venv/lib/python3.13/site-packages/pip/_internal/network/session.py": 293771075394156394, - "frontend/build/_app/immutable/nodes/35.BDALurfW.js": 9619547512402758415, - "backend/git_repos/remote/test-repo.git/objects/a1/3525f5f44e26d8fbec3226bd488d14793575a2": 3742572048019652365, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/WHEEL": 13232065379159720345, - "docs/api/html/group__RoleCreate.html": 982606771719996224, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____writeTabToUrl.html": 13401008738381827718, - "docs/api/html/group__AuthConfig.html": 8514504409080395912, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot_multilevel.py": 9830258607305392103, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_function_base.pyi": 2792940598392356268, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/base.py": 3628510830969664111, - "venv/lib/python3.13/site-packages/PIL/_imagingcms.pyi": 12476347428820346383, - "docs/api/html/group__frontend__src__lib__stores__sidebar__js____setMobileOpen.html": 14669790376571342620, - "venv/lib/python3.13/site-packages/anyio/to_thread.py": 12767326212338028587, - "docs/api/html/group__handleSort__Function.html": 14047089905040001393, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_null_file.py": 6885565068723353414, - "venv/lib/python3.13/site-packages/pyasn1/codec/native/decoder.py": 15336592439379775543, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_expanding.py": 12504275097476819918, - "backend/src/services/llm_provider.py": 17053465522943758939, - "venv/lib/python3.13/site-packages/numpy/_core/_simd.pyi": 13553578040186939249, - "venv/lib/python3.13/site-packages/websockets/cli.py": 14049106025886056110, - "docs/api/html/minus.svg": 5110402206540728274, - "docs/api/html/group__DashboardHub__getLlmSummaryLabel__Function.html": 17960034083462652821, - "docs/api/html/group__test__jwt__precedence.html": 11405311010293398777, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/use_modules.f90": 3913612933340746297, - "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django.py": 3210697476292094384, - "docs/api/html/group__DatasetReviewUs2WorkspaceUxTests__Module.html": 7172401692085262664, - "venv/lib/python3.13/site-packages/pandas/io/sas/sas7bdat.py": 17851617851457266799, - "venv/lib/python3.13/site-packages/git/objects/base.py": 11498407361286170399, - "venv/lib/python3.13/site-packages/pygments/styles/fruity.py": 7925019839136894457, - "docs/api/html/group__AssistantMessageRequest.html": 8567828667244420847, - "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.pyi": 18193727720492050602, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py": 15286893322149413712, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/resource_protector.py": 17080006010679257864, - "venv/lib/python3.13/site-packages/passlib/tests/test_pwd.py": 10089138951415591061, - "docs/api/html/group__ScheduleResponse.html": 597319143140494541, - "docs/api/html/group__frontend__src__services__adminService__js____getUsers.html": 5532590091702017666, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/appengine.py": 16194997685865850655, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_period.py": 12593777226419608212, - "docs/api/html/folderopen.svg": 11265655722809871940, - "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.pyi": 4729765666018003634, - "docs/api/html/group__readJson__Function.html": 17492082868349391161, - "docs/api/html/group__getReportTypeProfile__Function.html": 16691315682732378844, - "venv/lib/python3.13/site-packages/cffi/__init__.py": 13427266446746498548, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_conversion.py": 6205112277291935767, - "venv/lib/python3.13/site-packages/pandas/io/sql.py": 12592024569011796790, - "venv/lib/python3.13/site-packages/yaml/reader.py": 9873075843010693205, - "docs/api/html/group__list__reports.html": 7378159237170020810, - "venv/lib/python3.13/site-packages/pandas/io/sas/sasreader.py": 12766780210114366112, - "venv/lib/python3.13/site-packages/pygments/regexopt.py": 16073437303824961930, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_file.py": 5180401483285462901, - "frontend/src/components/DynamicForm.svelte": 2120031570302269345, - "backend/src/core/superset_client/__init__.py": 3185082867840025180, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numerictypes.py": 5719374287454872555, - "docs/api/html/navtreeindex15.js": 13503057042941155951, - "docs/api/html/group__test__semantic__source__version__propagation__preserves__locked__fields.html": 12577203451483874098, - "venv/lib/python3.13/site-packages/pydantic/_internal/_discriminated_union.py": 1818179910730401509, - "docs/api/html/group__dataset__websocket__endpoint.html": 3534578434362936086, - "venv/lib/python3.13/site-packages/pandas/core/strings/accessor.py": 14021462534679430426, - "venv/lib/python3.13/site-packages/dateutil/parser/isoparser.py": 18015430204844361067, - "venv/lib/python3.13/site-packages/pygments/lexers/haskell.py": 8011341851439354724, - "docs/api/html/group__DashboardHub__normalizeTaskStatus__Function.html": 805796678502738814, - "venv/lib/python3.13/site-packages/numpy/_core/_rational_tests.cpython-313-x86_64-linux-gnu.so": 13475521165331256711, - "docs/api/html/group__NativeFilterDataMask.html": 16691586180906605871, - "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.py": 16539612003028676816, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/quantile.py": 17341029201525362074, - "backend/src/api/routes/__tests__/test_reports_detail_api.py": 8519050847848949612, - "venv/lib/python3.13/site-packages/_pytest/junitxml.py": 5871715626272366062, - "docs/api/html/group__resolveTask__Function.html": 18339026573508690713, - "venv/lib/python3.13/site-packages/charset_normalizer/utils.py": 11724147997933584095, - "docs/api/html/group__test__i18n__usage.html": 8090230676674799070, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/assertion.py": 5287436128235488464, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_extension.py": 4831602527247293866, - "venv/lib/python3.13/site-packages/numpy/lib/_datasource.py": 18136853038949826147, - "docs/api/html/group____check__any__permission.html": 4366102498601457818, - "venv/lib/python3.13/site-packages/pygments/lexers/tcl.py": 10343667975436015855, - "venv/lib/python3.13/site-packages/pygments/lexers/graphql.py": 2307854666015224677, - "backend/src/core/migration/dry_run_orchestrator.py": 1628125044585364194, - "venv/lib/python3.13/site-packages/websockets/legacy/server.py": 5390090919193784186, - "docs/api/html/group__test__get__dashboards__invalid__pagination.html": 5025034056693178117, - "docs/api/html/group__SchedulerService.html": 9915139758062001823, - "docs/api/html/group__test__get__history__wraps__unexpected__error__as__500.html": 11007813825591521037, - "docs/api/html/group__test__get__datasets__pagination__oversize.html": 6499529865741709635, - "docs/api/html/group__orchestrator__cancel.html": 7070873577920045293, - "venv/lib/python3.13/site-packages/greenlet/tests/test_contextvars.py": 6077482318964702132, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_arrow.py": 10068866425352207113, - "docs/api/html/group____seed__legacy__repo.html": 8385616245583706934, - "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.cpython-313-x86_64-linux-gnu.so": 16963890771704942465, - "docs/api/html/group__SupersetCompilationAdapter____request__superset__preview.html": 13296042812426892350, - "docs/api/html/group__FastAPI__App.html": 16248117540359228223, - "docs/api/html/group____make__environment.html": 15812631705541506807, - "docs/api/html/group__InternalSourcesOnlyStage.html": 4233027249297243678, - "docs/api/html/navtreeindex11.js": 3487316622421924543, - "venv/lib/python3.13/site-packages/_pytest/raises.py": 8188402179490363767, - "frontend/src/routes/+page.svelte": 5863865830077771796, - "backend/src/api/routes/assistant/_routes.py": 17155281273786858236, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/ma/LICENSE": 6723325434476453127, - "docs/api/html/search/groups_b.js": 9594259470903340735, - "venv/lib/python3.13/site-packages/passlib/tests/sample1b.cfg": 2872366861156549016, - "docs/api/html/group__get__logs.html": 15112919543989403638, - "docs/api/html/search/all_3.js": 16759903393811042899, - "docs/api/html/group__LogLevel.html": 10025057817499119875, - "docs/api/html/group__mock__profile__route__dependencies.html": 17310608398693875616, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/secrets.py": 17816662030600756342, - "venv/lib/python3.13/site-packages/git/repo/__init__.py": 6173604284490891047, - "docs/api/html/group__orchestrator__run__completion.html": 9161921145624746021, - "docs/api/html/group__TestTranslateSchedulerGuard.html": 5632425831791394338, - "venv/lib/python3.13/site-packages/starlette/middleware/trustedhost.py": 8302185291610019926, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_half.py": 3121836756584726126, - "docs/api/html/group__SupersetAccountCandidate.html": 12015919113929683883, - "frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte": 13452471817010485891, - "docs/api/html/group__ValidationTaskList.html": 10113816788777682664, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_logical.py": 6818185860296232476, - "docs/api/html/group__ImportedFilter.html": 10182695392168978110, - "docs/api/html/group__InlineCorrectionService.html": 11403442380597184495, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/keyring/errors.py": 4587094338672775040, - "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/h11/_state.py": 11398755254809751139, - "docs/api/Doxyfile": 3726528055138990005, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_timedelta64.py": 16821719402662529276, - "docs/api/html/group__ReviewWorkspaceHeader.html": 7831080431402796410, - "docs/api/html/group__LaunchDatasetResult.html": 13328722234223010453, - "docs/api/html/group____resolve__dashboard__id__from__ref.html": 3342456016478632916, - "backend/src/core/superset_client/_datasets_preview_filters.py": 3588066821120121881, - "docs/api/html/group__fetchTargetDatabases__Function.html": 12217542332937976148, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/py.typed": 15130871412783076140, - "docs/api/html/group__internal__sources__only.html": 7674825160661281445, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/__init__.py": 15130871412783076140, - "docs/api/html/group__parse__position__json.html": 14006761960498316468, - "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.py": 13574106461973044500, - "docs/api/html/group__EnterpriseCleanSetupE2E.html": 12663560821891997943, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_datetime_index.py": 15448494560143263150, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py": 4111797647365870137, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/json.py": 14738429697319670035, - "backend/src/models/auth.py": 10012201935491426274, - "docs/api/html/test.html": 12181232123406114727, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__add__validation__job.html": 8326497628811170515, - "venv/lib/python3.13/site-packages/greenlet/greenlet_exceptions.hpp": 8819301638282185623, - "docs/api/html/search/groups_7.js": 14622460438011001410, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_data.py": 2397937327057341, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/distributions.h": 17162252287980242413, - "venv/lib/python3.13/site-packages/gitdb/db/ref.py": 1176701624460337371, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/_arrow_utils.py": 17808805047762566525, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_take.py": 14969455660160053080, - "docs/api/html/group__auth__config.html": 14653694386669705505, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py": 6678500392137188521, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_byteswap.py": 5090287710338893350, - "venv/lib/python3.13/site-packages/pandas/io/formats/csvs.py": 17223598248259691292, - "venv/lib/python3.13/site-packages/pygments/lexers/ambient.py": 18106929781201758080, - "docs/api/html/group__RepositoryBindingSchema.html": 7912861215581324084, - "venv/lib/python3.13/site-packages/packaging/markers.py": 11602308203217831543, - "backend/src/api/routes/maintenance/__init__.py": 8921716690761316245, - "backend/_batch_convert_defs.py": 2671571251303777016, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexing.py": 9988098331562906950, - "backend/src/api/routes/__tests__/test_reports_api.py": 12407027129865592126, - "venv/lib/python3.13/site-packages/pydantic/deprecated/decorator.py": 8609500261161560793, - "docs/api/html/globals_t.html": 18109595601088428353, - "venv/lib/python3.13/site-packages/requests/exceptions.py": 6474239743209354989, - "docs/api/html/group__no__external__endpoints.html": 4259752722362818397, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_dict.py": 16252265723192124609, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_openpyxl.py": 9542840527483618681, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.pyi": 15767546107856827543, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cbrt.csv": 8929553891827331780, - "backend/src/plugins/translate/service_target_schema.py": 5277952452702772295, - "venv/lib/python3.13/site-packages/pygments/lexers/pony.py": 2720635393743617141, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_filter.py": 2388681477981649268, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/RECORD": 10052481303799123207, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numeric.pyi": 17005748821769179511, - "frontend/src/lib/stores/__tests__/mocks/env_public.js": 17950613826406014914, - "docs/api/html/group__test__list__gitea__repositories__returns__payload.html": 8122355547255221781, - "docs/api/html/group__test__permission__denied__non__admin.html": 13139728334206364468, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_factorize.py": 17995673140216929494, - "backend/src/api/routes/dashboards/_listing_routes.py": 10092152880931335002, - "docs/api/html/group__test__manifest__service__existing__manifest__cannot__be__mutated.html": 856424001917918075, - "docs/api/html/group__SrcRoot.html": 9966390518366512890, - "venv/lib/python3.13/site-packages/uvicorn/server.py": 15714008348292243019, - "venv/lib/python3.13/site-packages/fastapi/middleware/trustedhost.py": 4285622102799027823, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_unary.py": 8046543893978198672, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dtype.py": 5336980126536664360, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/common/pythoncapi-compat/COPYING": 3474104874623144155, - "venv/lib/python3.13/site-packages/PIL/WmfImagePlugin.py": 15685062825106444247, - "venv/lib/python3.13/site-packages/PIL/report.py": 9428754440615681790, - "venv/lib/python3.13/site-packages/git/objects/__init__.py": 2110615482320355676, - "venv/lib/python3.13/site-packages/fastapi/testclient.py": 2606236077497278662, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_quoting.py": 1147992698412929658, - "backend/src/plugins/translate/orchestrator_runner.py": 6903994426268521649, - "venv/lib/python3.13/site-packages/pandas/core/arrays/masked.py": 13977195666947626206, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/config.py": 10707678943298497676, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_iter.py": 9232021002336641851, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_seed_sequence.py": 7687465232954593311, - "venv/lib/python3.13/site-packages/jsonschema/__init__.py": 6925762200139836824, - "docs/api/html/files.html": 4939049439273801457, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____clearOnCompleteCallback.html": 13631531026182073774, - "venv/lib/python3.13/site-packages/numpy/core/getlimits.py": 14257687540182552408, - "venv/lib/python3.13/site-packages/uvicorn/protocols/utils.py": 6147480543268737542, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/METADATA": 1684451390569121499, - "frontend/src/routes/+page.ts": 11160596731849880920, - "backend/src/models/dataset_review_pkg/_enums.py": 501173908863819198, - "venv/lib/python3.13/site-packages/starlette/config.py": 16509394679506300256, - "docs/api/html/group__test__environment__model.html": 11941389798995546650, - "docs/api/html/group__run__publish.html": 18279744766750814950, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/format": 12697928260201325593, - "venv/lib/python3.13/site-packages/pytest_asyncio/__init__.py": 8162213292669110211, - "docs/api/html/navtreeindex12.js": 15950560879981123236, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__DashboardHub__hasColumnFilter__Function.html": 16255620732519975073, - "backend/src/services/resource_service.py": 12557244514401781887, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/sqltypes.py": 2952996080192295188, - "venv/lib/python3.13/site-packages/pygments/lexers/hare.py": 8693002231020751711, - "venv/lib/python3.13/site-packages/pytest_asyncio/plugin.py": 13414541723622354359, - "venv/lib/python3.13/site-packages/pandas/__init__.py": 793698514004377486, - "venv/lib/python3.13/site-packages/pygments/lexers/fantom.py": 1986459352773446836, - "venv/lib/python3.13/site-packages/PIL/AvifImagePlugin.py": 1872412416505003364, - "frontend/src/components/tasks/LogEntryRow.svelte": 7224781044020990429, - "docs/api/html/group__test__tui__f8__blocked__without__facade__binding.html": 5329039055415769403, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_insert.py": 9195027887301180927, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_deprecations.py": 8310244516175542839, - "venv/lib/python3.13/site-packages/git/objects/tag.py": 7954808535585493300, - "docs/api/html/group__DashboardDatasetItem.html": 288797829356699689, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_frame.py": 2169381747938271444, - "docs/api/html/group__test__end__event__shared__banner.html": 116805575152722056, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_reindex.py": 15680924058960901067, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/RECORD": 13494296690334916257, - "venv/lib/python3.13/site-packages/httpcore/_sync/interfaces.py": 1532245423878428608, - "docs/api/html/group__AssistantSchemas.html": 17488044216136493605, - "docs/api/html/group__ProfileAuthorizationError.html": 17298627696052003006, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.pyi": 14233146512272397376, - "venv/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py": 991029278889407558, - "docs/api/html/group__detect__language.html": 18333706809382540759, - "venv/lib/python3.13/site-packages/pandas/io/formats/string.py": 14718123345899686696, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_quarter.py": 6795026338946382805, - "docs/api/html/group____recovery__imports.html": 14364048882219717740, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_arithmetic.py": 17449383588949771785, - "venv/lib/python3.13/site-packages/git/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_repr.py": 16609300578827398344, - "docs/api/html/group__SidebarTestModule.html": 15647744772485755211, - "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/fetch.py": 13937019222549208796, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_index_new.py": 796607958391882182, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/__init__.py": 3577295959173028042, - "docs/api/html/doxygen__docs_8h.html": 4085601024655800433, - "docs/api/html/group__SlackProvider.html": 13046100064758196543, - "docs/api/html/group__EventBusModule.html": 12486786797945687836, - "docs/api/html/group__TranslatePluginPackage.html": 16341267951062510196, - "docs/api/html/group__get__filename__from__headers.html": 457671362301086919, - "venv/lib/python3.13/site-packages/click/parser.py": 13988544851350599869, - "venv/lib/python3.13/site-packages/greenlet/greenlet_refs.hpp": 8525862687669040510, - "docs/api/html/group__TranslationJobDictionary.html": 12589120419165244757, - "docs/api/html/navtreeindex18.js": 12393713505269071662, - "docs/api/html/group__TestClickHouseTimestampNormalization.html": 16093633626248389892, - "docs/api/html/group__FilterConfidenceState.html": 10329024197508984898, - "backend/git_repos/remote/test-repo.git/hooks/pre-push.sample": 8029776382195911768, - "docs/api/html/group__BulkFindReplaceService.html": 13762621363378124184, - "backend/src/core/superset_client/_dashboards_write.py": 17768282769896196778, - "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.py": 546976622482819095, - "docs/api/html/group____build__target__columns.html": 5216268696007966716, - "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/__init__.py": 1229691061011958769, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_api.py": 7131036004750554905, - "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py": 17231166761215151699, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_sql.py": 3901697517087276687, - "frontend/static/favicon.svg": 6451919037497541980, - "venv/lib/python3.13/site-packages/_pytest/config/exceptions.py": 3199245296954654462, - "docs/api/html/group____FakeTask.html": 16676856916522433598, - "venv/lib/python3.13/site-packages/pandas/tests/computation/test_compat.py": 11655000938111420543, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_transform.py": 5674175311567817854, - "frontend/src/components/tasks/LogFilterBar.svelte": 1342324464094667837, - "venv/lib/python3.13/site-packages/numpy/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/agile.py": 5686162551416231457, - "docs/api/html/group__test__llm__analysis__timeout__constants.html": 8010871658971891725, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_py.py": 13809503317864539103, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.py": 4672871351134074735, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo90.f90": 3492659529812762861, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.cpython-313-x86_64-linux-gnu.so": 18139875206371157315, - "docs/api/html/group__LogFilterBar.html": 6090838125002308969, - "docs/api/html/group__test__multiple__events.html": 15612673565402865625, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/models.py": 17870758731501836303, - "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.pyi": 14181518821845356636, - "venv/lib/python3.13/site-packages/pygments/styles/igor.py": 2291640843781128270, - "frontend/build/_app/immutable/chunks/C2DipQLT.js": 333538434576244034, - "venv/lib/python3.13/site-packages/keyring/__main__.py": 14602212239605295451, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/METADATA": 14608873187265321271, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_compression.py": 8061288329598682752, - "backend/src/services/reports/__tests__/test_report_service.py": 2262310201779987221, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_dtype.py": 12210524152630532747, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_regression.py": 6742646011852052645, - "venv/lib/python3.13/site-packages/pandas/core/methods/__init__.py": 15130871412783076140, - "docs/api/html/group__DatasetProfileDto.html": 15394265151763865885, - "backend/src/core/superset_client/_databases.py": 16562767538012813426, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/__init__.py": 9083396324667433335, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args.py": 5821040955806257450, - "docs/api/html/search/mag_d.svg": 14208504129760637316, - "frontend/playwright-report/trace/index.CzXZzn5A.css": 10367596602957476705, - "docs/api/html/group__test__revoke__not__found.html": 17990266220085436024, - "docs/api/html/group__login__adfs.html": 10102492706907150145, - "backend/src/plugins/translate/preview_prompt_helpers.py": 14303328860208074708, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/WHEEL": 13347410390513723930, - "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5.html": 13431474567808681090, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/schema.py": 5391655516870611151, - "frontend/build/_app/immutable/chunks/DkSc7bnI.js": 16633882293462436801, - "docs/api/html/group__ValidationRunService.html": 9956169665748668204, - "docs/api/html/group__SupersetSqlLabExecutor.html": 4626317945232616372, - "venv/lib/python3.13/site-packages/cryptography/py.typed": 15130871412783076140, - "docs/api/html/group__wait__for__input.html": 11071282356984674501, - "docs/api/html/group__test__release__reject__contract.html": 11500420123173892128, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/METADATA": 13633829318759752611, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_take.py": 1216902953519121176, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_between.py": 337429135216881486, - "venv/bin/pip": 13861749540792881808, - "docs/api/html/group__frontend__src__services__taskService__js____getTasks.html": 12754837933544376454, - "venv/lib/python3.13/site-packages/passlib/totp.py": 14590474309565233318, - "venv/lib/python3.13/site-packages/cryptography/exceptions.py": 5173540025160237758, - "docs/api/html/group__test__enterprise__policy__valid.html": 195059262392650565, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_astype.py": 15768655715952923925, - "backend/src/models/dashboard.py": 18145369015035676491, - "venv/lib/python3.13/site-packages/pillow.libs/libbrotlicommon-c55a5f7a.so.1.2.0": 2635192149914499986, - "docs/api/html/group__frontend__src__lib__api__validation__js____buildParams.html": 8870797152431444795, - "docs/api/html/group__TaskStatus.html": 16939779299568979357, - "docs/api/html/group__ReportTypeProfiles__Module.html": 6048512366307639028, - "venv/lib/python3.13/site-packages/fastapi/utils.py": 15402403831700548944, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh26681.f90": 1515497186927052106, - "venv/lib/python3.13/site-packages/pygments/lexers/textfmts.py": 17438453283146671997, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_kurt.py": 2898715045429898166, - "venv/lib/python3.13/site-packages/pandas/_libs/interval.cpython-313-x86_64-linux-gnu.so": 8964760352224019544, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pyasn1/type/tag.py": 13912273274509186651, - "venv/lib/python3.13/site-packages/fastapi/security/api_key.py": 6965937678104291169, - "backend/src/core/encryption_key.py": 11618631956132703846, - "docs/api/html/group__test__republish__after__revoke__creates__new__active__record.html": 4995611949973079733, - "docs/api/html/group__test__clean__release__cli.html": 3665197736868730317, - "venv/lib/python3.13/site-packages/pygments/lexers/compiled.py": 8215290039991205531, - "frontend/src/lib/ui/Card.svelte": 8508366526326051165, - "docs/api/html/group__SupersetClientGetAllResources.html": 16201935844007355283, - "docs/api/html/group__SupersetContextRecoveryMixin____normalize__imported__filter__payload.html": 13846106026428255506, - "frontend/src/routes/dashboards/health/__tests__/health_page.integration.test.js": 7606382627471479368, - "docs/api/html/group__test__get__users__page__sends__lowercase__order__direction.html": 17648935033667456417, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/characteristics.py": 2916810597149066609, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/help.py": 3124654001287175629, - "venv/lib/python3.13/site-packages/apscheduler/executors/gevent.py": 17122637996105344344, - "docs/api/html/group__get__resource__service.html": 18383236607747383835, - "venv/lib/python3.13/site-packages/iniconfig/__init__.py": 12623265899068631092, - "docs/api/html/group__GiteaRepoCreateRequest.html": 610515655183349355, - "docs/api/html/group__test__task__context__preserves__background__tasks__across__sub__context.html": 15827171381080992984, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_strptime.py": 14328510310436552635, - "venv/bin/dotenv": 953953349803507198, - "frontend/src/lib/components/reports/__tests__/reports_list.ux.test.js": 10301023261856534116, - "docs/api/html/group__test__report__builder__sets__immutable__snapshot__flag.html": 4818862330013157215, - "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.pyi": 14558091964387571257, - "docs/api/html/group__ScreenshotService.html": 3636441712660943738, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_quantile.py": 17110581927381599978, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_character.py": 850554362222586199, - "frontend/build/_app/immutable/nodes/20.CrNL0ah0.js": 8059546893479032360, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/RECORD": 7320381591791379627, - "venv/lib/python3.13/site-packages/urllib3/util/proxy.py": 17667887856427739069, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh17859.f": 13562390882286135024, - "docs/api/html/group__get__scheduler__service.html": 12730756881612997283, - "docs/api/html/group__DatasetRunContext.html": 16196372668932517376, - "venv/lib/python3.13/site-packages/passlib/handlers/des_crypt.py": 15304795681178011265, - "venv/lib/python3.13/site-packages/requests/auth.py": 912521029864094489, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_clip.py": 11111183698178681331, - "venv/lib/python3.13/site-packages/numpy/fft/_helper.pyi": 9546918341939102632, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/WHEEL": 11630459739183915695, - "docs/api/html/group__DatasourceMetadataService.html": 6885799880168091624, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_win_type.py": 18105114803686556748, - "venv/lib/python3.13/site-packages/httpcore/_models.py": 2501866419898326733, - "venv/lib/python3.13/site-packages/pandas/tests/resample/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/orchestrator_cancel.py": 673611364664367049, - "venv/lib/python3.13/site-packages/anyio/_core/_testing.py": 14024940365851109155, - "docs/api/html/group__get__user__by__username.html": 2246686587123622263, - "docs/api/html/group__TranslationResultAggregator.html": 7516401936647746773, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending.pyx": 756189645898997788, - "venv/lib/python3.13/site-packages/numpy/_core/defchararray.pyi": 2277933368373498515, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/RECORD": 9964194961227523130, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_at.py": 1029744344133153888, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_getitem.py": 11882213323878588707, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598.f90": 1409455010615482779, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/common.py": 2075178842766962782, - "venv/lib/python3.13/site-packages/urllib3/fields.py": 9503202008440141421, - "docs/api/html/group__ui__Module.html": 3556271867573996475, - "frontend/src/lib/stores/sidebar.js": 4584261073573678043, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/__init__.py": 96341051676048910, - "venv/lib/python3.13/site-packages/pygments/lexers/testing.py": 17148598391210353576, - "frontend/src/lib/components/health/ScheduleAtAGlance.svelte": 2047418178814771337, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/__init__.py": 1437580234680265326, - "docs/api/html/group__test__commit__changes__wraps__unexpected__error__as__500.html": 2137933037595901582, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/recarray_from_file.fits": 15116318600514274152, - "venv/lib/python3.13/site-packages/pandas/api/typing/aliases.py": 11194270368147942988, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_frame.py": 9378623452601244234, - "frontend/playwright-report/index.html": 73860403936737424, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_constructors.py": 11786151805350138436, - "frontend/build/_app/immutable/nodes/31.B1endBte.js": 11672575267879175465, - "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.py": 12771179468704314203, - "venv/lib/python3.13/site-packages/pandas/io/excel/_util.py": 6664989864423807444, - "venv/lib/python3.13/site-packages/git/cmd.py": 8854786163152637148, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libk5crypto-b1f99d5c.so.3.1": 10669523137596317954, - "frontend/src/components/backups/BackupList.svelte": 7178110735645066783, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_parser.py": 8145921552236189367, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_equals.py": 6429101196875333574, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/rsa/prime.py": 10820898885044917095, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/parameters.py": 5104015814292812973, - "docs/api/html/group__TestGitStatusRoute.html": 2805903254981550979, - "docs/api/html/group__save__profile__and__findings.html": 2929839953576621200, - "venv/lib/python3.13/site-packages/pandas/_libs/algos.pyi": 22405377601029952, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/einsumfunc.pyi": 8902765738896588686, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py": 5450931608244830335, - "frontend/src/routes/datasets/review/useReviewSession.js": 2360677870127730232, - "venv/lib/python3.13/site-packages/psycopg2/tz.py": 9966680685796585198, - "venv/lib/python3.13/site-packages/pygments/lexers/bdd.py": 5389493859905397060, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_docs.py": 17898499890274685198, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polyutils.pyi": 3841660741524427413, - "docs/api/html/group__EnvironmentMock__Module.html": 14116359480209547116, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/__init__.py": 15130871412783076140, - "docs/api/html/group__test__create__remote__repository__creates__provider__repo.html": 13400861927477164469, - "venv/lib/python3.13/site-packages/pygments/lexers/web.py": 11104721221615804497, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x32_unix.h": 9165722966153119887, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed": 15130871412783076140, - "docs/api/html/group__DatasetsApiTests.html": 8138723528945535385, - "venv/lib/python3.13/site-packages/_pytest/assertion/util.py": 16957445183087315124, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/trafficscript.py": 776604244051744646, - "venv/lib/python3.13/site-packages/websockets/legacy/__init__.py": 12332512329533591682, - "docs/api/html/group__TermCorrectionBulkSubmit.html": 8087312419241004448, - "docs/api/html/group__test__get__next__executions.html": 17453367342374515739, - "venv/lib/python3.13/site-packages/itsdangerous/signer.py": 3694661094753475840, - "venv/lib/python3.13/site-packages/numpy/lib/array_utils.py": 18135114588643176359, - "frontend/src/lib/stores/__tests__/sidebar.test.js": 3057168376995255911, - "docs/api/html/group__test__parse__dashboard__url__for__filters__permalink.html": 5330991424170288666, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/expression.py": 15511250785671695359, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_update_delete.py": 1317337950621091021, - "docs/api/html/group__test__run__without__manifest__rejected.html": 13460920870647693397, - "docs/api/html/group__build__columns.html": 14782239725954054542, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_keywords.py": 11487954623214475915, - "frontend/build/_app/immutable/chunks/B5kMH_bQ.js": 13942101361562838660, - "docs/api/html/group____parse__command.html": 2613491451304576155, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_unix.h": 13709837360414450184, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/hotp.py": 18325640697829098192, - "backend/src/services/auth_service.py": 5948958621485759282, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/linalg/__init__.pyi": 18409133021422984774, - "docs/api/html/group__frontend__src__services__git__utils__js____resolveCurrentEnvironmentId.html": 18338234361034582529, - "docs/api/html/group__normalize__owner__tokens.html": 10173921796259136968, - "venv/lib/python3.13/site-packages/pandas/core/indexing.py": 15507869313676722071, - "docs/api/html/group____broadcast__dataset__updated.html": 8967669920658939702, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_formats.py": 12299393760307251036, - "docs/api/html/group____handle__response__format__fallback.html": 11346867985412676592, - "backend/src/services/clean_release/__init__.py": 9144880924791476391, - "venv/lib/python3.13/site-packages/cryptography/x509/certificate_transparency.py": 11337143186866439208, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/RECORD": 10552906886624188222, - "docs/api/html/group__update__execution__mapping.html": 5866096724549130972, - "docs/api/html/group__test__belief__scope__success__reflect.html": 8691621763793235095, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/INSTALLER": 17282701611721059870, - "backend/src/plugins/translate/_llm_call.py": 12276699540223140419, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo77.f": 12000068001851270396, - "frontend/src/lib/api/translate/target-schema.js": 10665479411127476571, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/RECORD": 5612689057373270213, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/eoo.py": 5126566925877730303, - "docs/api/html/navtreeindex2.js": 14264443857454783261, - "venv/lib/python3.13/site-packages/_pytest/skipping.py": 10561585274103209115, - "backend/src/core/auth/jwt.py": 16970967561039664297, - "docs/api/html/group__MigrationEngine.html": 9187249700735075008, - "docs/api/html/group__get__database__mappings.html": 4882039116942012048, - "docs/api/html/group__AssistantModels.html": 13818196745867110031, - "venv/lib/python3.13/site-packages/fastapi/websockets.py": 9910052969532075719, - "backend/src/schemas/health.py": 17351928434071690723, - "venv/lib/python3.13/site-packages/authlib/oauth2/client.py": 7237359250450222680, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/bitwise_ops.pyi": 15598239133239520456, - "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.py": 2215312866861772848, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/rsa_key.py": 7092106298733667895, - "venv/lib/python3.13/site-packages/anyio/_core/_fileio.py": 4507100568401692983, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_case_justify.py": 8204537837649550062, - "venv/lib/python3.13/site-packages/rsa/transform.py": 12261738253972114335, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/__init__.py": 13841777369501476946, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/url.py": 1558176735588971829, - "venv/lib/python3.13/site-packages/pygments/lexers/jsonnet.py": 1314044008440065525, - "docs/api/html/group__read__dashboard__from__disk.html": 6267975408342998536, - "docs/api/html/group__frontend__src__routes____page__ts____load.html": 10563006296048613280, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_index.py": 10837492069089787028, - "venv/lib/python3.13/site-packages/pydantic/v1/main.py": 213087273729911492, - "docs/api/html/group__test__approveRow__Function.html": 17163537917081477745, - "venv/lib/python3.13/site-packages/starlette/templating.py": 9242638124487797988, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_constructors.py": 9815164146739254076, - "venv/lib/python3.13/site-packages/pygments/formatters/bbcode.py": 8614391937525677922, - "docs/api/html/group__loadRecentTasks__Function.html": 8342396704682270870, - "venv/lib/python3.13/site-packages/pygments/styles/algol.py": 3040373291664740876, - "backend/src/plugins/translate/preview_token_estimator.py": 254295902082954386, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_coercion.py": 7261787308019145312, - "docs/api/html/search/groups_d.js": 1277170985754207380, - "docs/api/html/group__BuildOfflineDockerBundle.html": 2244041374994039539, - "venv/lib/python3.13/site-packages/greenlet/TMainGreenlet.cpp": 9489520001674089404, - "venv/lib/python3.13/site-packages/pygments/lexers/_postgres_builtins.py": 2383906454995727700, - "venv/lib/python3.13/site-packages/fastapi/_compat/model_field.py": 392699021682253569, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/prompt.py": 9561640601516933380, - "venv/lib/python3.13/site-packages/pygments/lexers/dns.py": 17149871769553794654, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____createMaintenanceStore.html": 15666277817262057243, - "docs/api/html/group__SessionCollaboratorRole.html": 3015568436480767444, - "docs/api/html/group__get__settings.html": 1490728116106832265, - "docs/api/html/group__frontend__src__lib__api__validation__js____triggerRun.html": 4533127585714595290, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_hour.py": 14128508606361694814, - "venv/lib/python3.13/site-packages/pandas/_libs/testing.cpython-313-x86_64-linux-gnu.so": 16758333404587340103, - "docs/api/html/group__updateTask__Function.html": 11102333396610846970, - "docs/api/html/group__SettingsPage.html": 11139530217038362934, - "docs/api/html/group__resolve__provider__model.html": 3697548583109098137, - "docs/api/html/group__test__valid__key__returns__principal.html": 14343096821860232032, - "venv/lib/python3.13/site-packages/itsdangerous/serializer.py": 14271229091461164933, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_copy.py": 12902342280706372953, - "docs/api/html/group__fetchTasks__Function.html": 2358966877665297910, - "venv/lib/python3.13/site-packages/pluggy/__init__.py": 8269188363563356631, - "docs/api/html/group__Permission.html": 12943669349918425540, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_concat.py": 16549010224568175202, - "frontend/src/lib/i18n/index.ts": 14616557360624297847, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_resolution.py": 9651416588266430520, - "backend/ss_tools_dev.db": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timezones.py": 8042029178538467146, - "docs/api/html/group__InlineCorrectionSubmit.html": 13969595502641036335, - "docs/api/html/group__create__isolated__repository.html": 15487512963939648991, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_laguerre.py": 7475592201750602042, - "venv/lib/python3.13/site-packages/pandas/core/arrays/numpy_.py": 16641729409992576358, - "venv/lib/python3.13/site-packages/h11/__init__.py": 222801976520197308, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/__init__.py": 4781700006554860887, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/aead.pyi": 13061638961405460334, - "venv/lib/python3.13/site-packages/numpy/random/__init__.py": 11732100564412485633, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/__init__.py": 14522970260621605208, - "venv/lib/python3.13/site-packages/pygments/formatters/terminal256.py": 11473927781167132440, - "docs/api/html/group__TokenData.html": 10513747130917299744, - "venv/lib/python3.13/site-packages/anyio/_core/_tasks.py": 14104828341875599130, - "docs/api/html/search/groups_16.js": 8625558773447853349, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/WHEEL": 8954358347596196608, - "backend/src/plugins/translate/service_bulk_replace.py": 5355744069308185607, - "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/trace.zip": 2929961739056234978, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/__init__.py": 15130871412783076140, - "docs/api/html/group__client.html": 9166531319498298577, - "venv/lib/python3.13/site-packages/secretstorage/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api1.c": 8494977275215578998, - "frontend/src/components/git/GitWorkspacePanel.svelte": 4516972512005887443, - "venv/lib/python3.13/site-packages/pygments/lexers/ptx.py": 4855144232064607995, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.pyi": 2700568594438441246, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_find_common_type.py": 9581516171321396333, - "docs/api/html/globals_func_t.html": 17227441317231061430, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_datetime.py": 13781712980921881470, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_periodindex.py": 13430003945574795792, - "venv/lib/python3.13/site-packages/starlette/middleware/httpsredirect.py": 16837082808103815044, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh18335.f90": 1126438609189294255, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/shape.py": 13410030168622484801, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/__init__.py": 15130871412783076140, - "docs/api/html/group__handleDeleteReport__Function.html": 6406518708534911213, - "docs/api/html/group____persist__audit.html": 17877024820327467143, - "venv/lib/python3.13/site-packages/greenlet/tests/test_extension_interface.py": 11693583111513457974, - "venv/lib/python3.13/site-packages/pygments/lexers/modeling.py": 15714077241620129689, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/distro.py": 12494639376332486204, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_update.py": 12293424926920594944, - "docs/api/html/group__frontend__src__services__adminService__js____getPermissions.html": 986367622820934646, - "docs/api/html/group__MigrationDryRunService.html": 12712480299293943779, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_utils.py": 2329717399120006435, - "backend/src/plugins/translate/_lang_detect.py": 2125979777533490081, - "docs/api/html/group__ReportCollection.html": 7174779192702599484, - "docs/api/html/group__MigrationE2E.html": 11325763991415698239, - "docs/api/html/group__CompiledPreview.html": 13362113672066090539, - "docs/api/html/group____normalize__filter__values.html": 7790645858095652627, - "docs/api/html/group__delete__file.html": 5881932655140602026, - "venv/lib/python3.13/site-packages/gitdb/db/loose.py": 17467413653016810702, - "venv/lib/python3.13/site-packages/_pytest/stash.py": 14430252294763173610, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_quantile.py": 4225791949463893856, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/typst.tpl": 5055419073126793137, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/RECORD": 2963644632913976754, - "venv/lib/python3.13/site-packages/apscheduler/__init__.py": 8652514118183227412, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_info.py": 15151894356374015812, - "venv/lib/python3.13/site-packages/pyasn1/type/error.py": 1479878696626082697, - "venv/lib/python3.13/site-packages/PIL/DcxImagePlugin.py": 11767274218348117481, - "frontend/build/_app/immutable/chunks/ZvPSSfIA.js": 7807067580962053496, - "venv/lib/python3.13/site-packages/pandas/core/_numba/__init__.py": 15130871412783076140, - "docs/api/html/group____get__default__environment__id.html": 17375226256254872481, - "venv/lib/python3.13/site-packages/pygments/lexers/arturo.py": 4565524340706235725, - "docs/api/html/group__SessionDetail.html": 8883911469141750019, - "docs/api/html/group____extract__chart__data__rows.html": 2508559388931054401, - "docs/api/html/group____make__us3__session.html": 16077792546481746027, - "backend/src/core/task_manager/graph.py": 3769962371015813191, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_asof.py": 15792528637923603240, - "docs/api/html/group__AuthService.html": 17583408280713591083, - "backend/src/plugins/translate/orchestrator_config.py": 1227864697811313446, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_copy_deprecation.py": 16045426957636349035, - "docs/api/html/group__approve__batch__execution__mappings.html": 17647944553899662758, - "venv/lib/python3.13/site-packages/pandas/io/excel/_xlsxwriter.py": 16873939324418752965, - "docs/api/html/group__ScriptsPackage.html": 4686762520162845714, - "frontend/src/lib/components/DashboardMaintenanceBadge.svelte": 9085495636438661604, - "docs/api/html/group____seed__candidate__with__report.html": 5421169190057147596, - "docs/api/html/group__MappingsPageScript__Block.html": 3290002320641902083, - "backend/src/plugins/storage/plugin.py": 8516872254456553013, - "venv/lib/python3.13/site-packages/cffi/_cffi_errors.h": 1479575326813300292, - "backend/git_repos/remote/test-repo.git/objects/71/47ad0836409b35c1650dddf5fa6329ba61a824": 16601919569600972016, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_csv.py": 571991535656576735, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_defmatrix.py": 2982572083770917411, - "docs/api/html/group__SupersetClientGetNativeFilterState.html": 7632236885296955181, - "venv/lib/python3.13/site-packages/anyio/abc/_subprocesses.py": 4297459042599291531, - "venv/lib/python3.13/site-packages/numpy/_typing/__init__.py": 18242238390157333247, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_repr.py": 7552782621374439569, - "venv/lib/python3.13/site-packages/pandas/_testing/_hypothesis.py": 12298650110193262367, - "backend/src/models/dataset_review_pkg/__init__.py": 1796062139548584878, - "backend/git_repos/remote/test-repo.git/objects/f7/6ee984d9d47c3cd0b362fdf60a47162bbc202d": 5351983130474596028, - "venv/lib/python3.13/site-packages/fastapi/security/base.py": 15674297568104917749, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/timeout.py": 1807228452024740476, - "docs/api/html/group__deleteRun__Function.html": 15413700130406237535, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py": 10379929935087904974, - "docs/api/html/group__MapperTool.html": 2827767962983201486, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/value_attrspec/gh21665.f90": 11945588837236968550, - "docs/api/html/group__get__merge__conflicts.html": 13270034822172825745, - "venv/lib/python3.13/site-packages/pygments/lexers/rego.py": 16022844238903541738, - "docs/api/html/group__test__register__candidate__rejects__duplicate__candidate__id.html": 6105187651633201559, - "docs/api/html/group____plan__intent__with__llm.html": 11560361184946600835, - "docs/api/html/group__test__git__service__get__repo__path__guard.html": 14019768017372580436, - "docs/api/html/group__test__duplicate__job.html": 16156801753517777022, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_get_numeric_data.py": 5896318696417090902, - "venv/lib/python3.13/site-packages/httpcore/_sync/http11.py": 5947339024588907032, - "docs/api/html/group__test__plugin__loader__uses__logger__not__print.html": 9927001174578654853, - "docs/api/html/group__SupersetClientUpdateDataset.html": 9267713141598532121, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/resolution.py": 5132658725516243311, - "docs/api/html/group__SupersetContextFiltersExtractMixin____extract__imported__filters.html": 5149922330981040989, - "docs/api/html/group__test__git__config.html": 14894041378515335606, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_timezones.py": 15157687716502574038, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/missing.py": 14589973874359353799, - "venv/lib/python3.13/site-packages/pandas/core/roperator.py": 2587829042468916489, - "docs/api/html/group__loadStatus__Watcher.html": 5559550850267014121, - "docs/api/html/group__TestAPIKeyRoutes.html": 4190666277064889784, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/asyncio.py": 2517623574187179319, - "venv/lib/python3.13/site-packages/uvicorn/middleware/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/registration.py": 14639972512303032712, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_fixed.f90": 8079594129222364031, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_encoding.py": 6622474614967398246, - "venv/lib/python3.13/site-packages/pygments/styles/borland.py": 17570558758932580659, - "docs/api/html/group__TranslationLanguageResponse.html": 17228524673941050506, - "venv/lib/python3.13/site-packages/iniconfig/_version.py": 8497474654470220170, - "docs/api/html/group____handle__http__error.html": 9220062881002809882, - "docs/api/html/search/functions_16.js": 14777540345560864351, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/__init__.py": 12849371269359608058, - "docs/api/html/group__run__revoke.html": 8815559895593475894, - "docs/api/html/doxygen.svg": 12256108328721719854, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/__init__.py": 9751988289698484530, - "venv/lib/python3.13/site-packages/urllib3/util/connection.py": 15511685694589961094, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkeyutils-dfe70bd6.so.1.5": 9414227432578104677, - "venv/lib/python3.13/site-packages/pygments/lexers/templates.py": 16267123844830647955, - "venv/lib/python3.13/site-packages/PIL/Image.py": 13615925856053031482, - "venv/lib/python3.13/site-packages/pygments/lexers/vip.py": 11287963054551122004, - "frontend/build/_app/immutable/nodes/2.D23is_vV.js": 13661853902901523795, - "backend/src/api/routes/validation/__tests__/test_validation_api.py": 3854767876193048673, - "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.pyi": 2744889206277418389, - "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.py": 151202984060101867, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/wrappers.py": 315409799569602977, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_arithmetic.py": 11620920548943067949, - "docs/api/html/group__normalizeDatasetReviewDetail__Function.html": 8804263811893076635, - "docs/api/html/group__clean__release__mappers.html": 5318141435059670889, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust.abi3.so": 2537152913308578487, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args_and_kwargs.py": 3381939712123764154, - "docs/api/html/group__handleCreateMapping__Function.html": 12147099060978125723, - "docs/api/html/group__MarkerLogger.html": 12923422177561303819, - "venv/lib/python3.13/site-packages/pillow.libs/libfreetype-ee1c40c4.so.6.20.4": 12301510769231120312, - "dist/docker/backend.e2e-test-20260526-112457.tar.xz": 4108388229722907376, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_util.py": 13997184496924925110, - "venv/lib/python3.13/site-packages/pillow.libs/libzstd-761a17b6.so.1.5.7": 17875236748140157032, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/provision.py": 8707620698060818654, - "docs/api/html/group__stage__result__map.html": 18249886114510031003, - "frontend/eslint.config.js": 14296985543214702398, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/METADATA": 3814104793342833872, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlrd.py": 9982457234958102003, - "venv/lib/python3.13/site-packages/PIL/PdfParser.py": 13710342183037972042, - "venv/lib/python3.13/site-packages/pandas/_libs/__init__.py": 17117112389651696580, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_validators.py": 14178329676643700151, - "venv/lib/python3.13/site-packages/itsdangerous/_json.py": 4872267972688724005, - "docs/api/html/group__DashboardMetadata.html": 3536240584361293053, - "venv/lib/python3.13/site-packages/certifi/core.py": 7472627735517449185, - "venv/lib/python3.13/site-packages/anyio/_core/__init__.py": 15130871412783076140, - "docs/api/html/group__SupersetClientGetDatabase.html": 4802291779474339990, - "docs/api/html/group__isAdminUser__Function.html": 2409880753894519871, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_index_as_string.py": 292438690259156869, - "venv/lib/python3.13/site-packages/httpx/_transports/asgi.py": 12335675489870745729, - "docs/api/html/group__test__multiple__stale__pending__all__cleared.html": 9272463816474981933, - "frontend/src/components/git/GitOperationsPanel.svelte": 5140779139910068800, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/bitgen.h": 12421210767114721499, - "docs/api/html/group__trigger__preview__generation.html": 17109618387126264866, - "docs/api/html/group__SqlTableExtractorModule.html": 7193260644238587422, - "venv/lib/python3.13/site-packages/numpy/_typing/_shape.py": 12667553664122376205, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_equals.py": 9405821443172167066, - "docs/api/html/group__test__pagination__boundaries.html": 7157080918011904103, - "docs/api/html/group__test__key__table__sorting.html": 7072979890307169502, - "venv/lib/python3.13/site-packages/passlib/utils/compat/_ordered_dict.py": 14248790502300368024, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_set_value.py": 4166411740676564820, - "docs/api/html/group__test__save__config__accepts__raw__payload__and__keeps__extras.html": 11846202691943319524, - "venv/lib/python3.13/site-packages/pydantic/v1/class_validators.py": 6471688474853407718, - "venv/lib/python3.13/site-packages/sqlalchemy/exc.py": 6372950288664776625, - "frontend/e2e/tests/migration.e2e.js": 7453107045608888506, - "venv/lib/python3.13/site-packages/pandas/core/frame.py": 2630807744775247533, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isin.py": 14387850595546682314, - "venv/lib/python3.13/site-packages/numpy/fft/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/pkg_resources/__init__.py": 1677532855362260338, - "docs/api/html/group__ValidationHistory.html": 75479766067234130, - "docs/api/doxygen_docs.h": 7306291205722937283, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_describe.py": 362374422228593281, - "venv/lib/python3.13/site-packages/passlib/handlers/mysql.py": 11165762635383041460, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_indexing.py": 9862204223281834360, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/impl.py": 4687705587963980337, - "backend/src/models/storage.py": 14886607138033931307, - "venv/lib/python3.13/site-packages/pillow.libs/libwebpmux-7f11e5ce.so.3.1.2": 13105940093474848953, - "docs/api/html/group__GitStatus.html": 12469885171313367595, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename_axis.py": 12432900682994622455, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_cte.py": 9274103293201050794, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/base.py": 6348888156686452825, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccosh.csv": 5882025767947638305, - "venv/lib/python3.13/site-packages/passlib/crypto/_md4.py": 379335134151814809, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_explode.py": 18263694040596159591, - "docs/api/html/group__seed__trace__id.html": 9478009198218783393, - "docs/api/html/group____require__auto__review__flag.html": 8794268766415982314, - "venv/lib/python3.13/site-packages/pandas/tests/test_optional_dependency.py": 7930709646653025355, - "docs/api/html/group____perform__upload.html": 16003778061143161761, - "docs/api/html/group__get__dashboard__filter__binding.html": 2139882779492208369, - "docs/api/html/group____resolve__repository__status.html": 14395426941616609866, - "docs/api/html/group____build__sql__lab__redirect__url.html": 4568202178269138535, - "docs/api/html/group__test__get__task__not__found.html": 13878984338730956196, - "docs/api/html/group__DatasetReviewSchemaDtos.html": 4934342432845855674, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion": 2607512361823362698, - "venv/lib/python3.13/site-packages/pandas/io/html.py": 2727247063231114153, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_cython_aggregations.py": 10818895573637904764, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setLoading.html": 8035127119828589627, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_stmts.f90": 6123192245816905092, - "docs/api/html/group__get__merge__status.html": 5585461605770877372, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_xs.py": 11073752361562791165, - "venv/lib/python3.13/site-packages/PIL/QoiImagePlugin.py": 17663704273100331654, - "venv/lib/python3.13/site-packages/pydantic/errors.py": 3506829204186508192, - "frontend/src/lib/toasts.js": 17626443524470260846, - "backend/alembic/versions/f0e9d8c7b6a5_cascade_ondelete_all_env_fks.py": 18274103435952144919, - "docs/api/html/tab_h.png": 1719686501894845356, - "venv/lib/python3.13/site-packages/httpcore/_sync/http_proxy.py": 14037211594707072958, - "frontend/src/lib/components/translate/TargetSchemaHint.svelte": 9354096644840520484, - "docs/api/html/group__ApiKeyRevokeResponse.html": 17229380547322413204, - "venv/lib/python3.13/site-packages/attr/_version_info.py": 3850449653182103457, - "venv/lib/python3.13/site-packages/_pytest/mark/__init__.py": 9343015549300275714, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/getitem.py": 556339939822508169, - "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.py": 16859854811012717088, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_join.py": 17508073052750406350, - "docs/api/html/group__ReportCardTest__Module.html": 2682480122400206889, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/__init__.py": 8632038387100622009, - "frontend/build/_app/immutable/nodes/6.lH4o9phB.js": 11604714069031543912, - "docs/api/html/group__RemoteRepoCreateRequest.html": 12603451785316041896, - "docs/api/html/group__test__create__job__invalid__upsert__strategy.html": 1478408184279931764, - "frontend/build/_app/immutable/nodes/11.DTcqXkiC.js": 9346257290917656558, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_classes.py": 1440926036579216168, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/licenses/LICENSE": 6653202620765194772, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/collections.py": 16114931075157411913, - "docs/api/html/group____dispatch__dataset__review__intent.html": 12800069212370219281, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/claims.py": 11853156183819186599, - "docs/api/html/group__get__branches.html": 10322130489312145131, - "venv/lib/python3.13/site-packages/apscheduler/job.py": 1858376671986108667, - "venv/lib/python3.13/site-packages/dotenv/__main__.py": 16883579879172186355, - "venv/lib/python3.13/site-packages/pygments/lexers/graphviz.py": 15677908982985025500, - "docs/api/html/group__frontend__src__services__taskService__js____getTaskLogs.html": 14401072635225830595, - "frontend/build/_app/immutable/chunks/DaqvCktX.js": 12117033425016065257, - "docs/api/html/group__list__roles.html": 13386931894504978817, - "docs/api/html/group__PrepareCandidateRequest.html": 3237622670833360342, - "venv/lib/python3.13/site-packages/idna/compat.py": 9758194415584776667, - "docs/api/html/group__test__key__cols__none__returns__all.html": 8776324869922559549, - "docs/api/html/group__test__stale__pending__cleared__and__proceeds.html": 15143795866405966436, - "venv/lib/python3.13/site-packages/jsonschema/tests/_suite.py": 6307666366417438489, - "docs/api/html/group__ExecutionMapping.html": 9160979664327901038, - "docs/api/html/group__startPolling__Function.html": 11240654189362337669, - "docs/api/html/group__add__backup__job.html": 16776188740195692544, - "venv/lib/python3.13/site-packages/pip/_internal/models/wheel.py": 10473287225032326206, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape_base.pyi": 5046405609030418900, - "venv/lib/python3.13/site-packages/pandas/core/reshape/encoding.py": 12702053687535473061, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/git/objects/submodule/util.py": 8621897576747443758, - "venv/lib/python3.13/site-packages/pygments/lexers/codeql.py": 4254046699692048717, - "docs/api/html/group__test__get__reports__invalid__filter__returns__400.html": 7652617513834271963, - "docs/api/html/group__ReportDetailIntegrationTest__Module.html": 11014134516097175898, - "docs/api/html/group__SemanticSource.html": 8217407902309590848, - "docs/api/html/group__test__extract__native__filters__from__permalink__direct__response.html": 2976726265511560969, - "docs/api/html/group__DatasetReviewFindingModels.html": 8713463567973015699, - "docs/api/html/group__normalize__start__page.html": 845187168959113829, - "frontend/src/lib/components/translate/BulkCorrectionSidebar.svelte": 5121541182559165710, - "backend/src/plugins/translate/__init__.py": 12400577052797809611, - "docs/api/html/group__test__expired__key__raises__401.html": 14311462003347056857, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test__exceptions.py": 12099001631178696189, - "venv/lib/python3.13/site-packages/pandas/core/arrays/string_arrow.py": 16997970607998635570, - "backend/src/plugins/git/__init__.py": 2144831474780401625, - "docs/api/html/group__test__get__datasets__service__failure.html": 7841926539827711165, - "docs/api/html/group__GitLLMExtension.html": 13438682288527399729, - "docs/api/html/group__normalize__username.html": 9149179080201447778, - "venv/lib/python3.13/site-packages/_pytest/_argcomplete.py": 3586514420674089892, - "venv/lib/python3.13/site-packages/pygments/lexers/comal.py": 769727871242464458, - "docs/api/html/bc_s.png": 11826155430197355290, - "docs/api/html/group__SupersetContextExtractorBase____search__nested__numeric__key.html": 8181622518576542399, - "docs/api/html/group__AssistantResolvers.html": 2791936012509790266, - "venv/lib/python3.13/site-packages/passlib/handlers/roundup.py": 3844280972451753019, - "docs/api/html/group__get__dashboard__async.html": 462527770103196028, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_constructors.py": 9767900500541600486, - "frontend/src/components/TaskRunner.svelte": 12776680477391915065, - "docs/api/html/group__apply__language__action.html": 6258130513680615487, - "docs/api/html/group__test__json__load__if__needed__json__string.html": 13679424805613281118, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/structures.py": 2668010839316715865, - "venv/lib/python3.13/site-packages/jose/__init__.py": 15575670403380555945, - "backend/src/scripts/test_dataset_dashboard_relations.py": 11883199369340933897, - "docker/nginx.ssl.conf": 12350396037548059686, - "docs/api/html/group__UpdateSessionRequest.html": 6044024401135206762, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_categorical_equal.py": 7942323667866280872, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_is_monotonic.py": 12769599198035357753, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/METADATA": 6960169108624303030, - "docs/api/html/group__PluginBase.html": 12795916349382052793, - "venv/lib/python3.13/site-packages/pygments/lexers/supercollider.py": 15306357945850389082, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/simple.py": 11050636051019777317, - "docs/api/html/group__get__job__metrics.html": 440506911200266424, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py": 335201628064585213, - "venv/lib/python3.13/site-packages/pip/_internal/req/constructors.py": 17485271035479378662, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_compat.h": 6512969159139431485, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex.py": 8772423071604000084, - "venv/lib/python3.13/site-packages/pycparser/_build_tables.py": 569728086093219621, - "docs/api/html/group__get__permission__by__id.html": 9650598973405100772, - "docs/api/html/group__SupersetContextParsingMixin__parse__superset__link.html": 5643145243445652253, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_numba.py": 1548992314517942530, - "frontend/build/_app/immutable/chunks/Cs7qTEqk.js": 15866070137921022262, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_federal.py": 16758212916399857971, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py": 16951268065271931235, - "venv/lib/python3.13/site-packages/urllib3/connectionpool.py": 7493427857878469165, - "frontend/src/routes/profile/__tests__/fixtures/profile.fixtures.js": 11794595959206318249, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/WHEEL": 2357997949040430835, - "docs/api/html/group__SearchPlugin.html": 13020517702009127534, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/dynamic.py": 15220116196072524330, - "docs/api/html/group__SupersetContextExtractorBase__build__recovery__summary.html": 4966078538740573780, - "venv/lib/python3.13/site-packages/requests/compat.py": 5836590172733661320, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/oracledb.py": 7158791242292470472, - "venv/lib/python3.13/site-packages/pygments/lexers/prolog.py": 599539463980699865, - "venv/lib/python3.13/site-packages/urllib3/contrib/pyopenssl.py": 11839715589709210412, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_repeat.py": 9155935990114743583, - "backend/src/plugins/translate/_text_cleaner.py": 13161219043687449729, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayterator.pyi": 7439348655567232744, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_converter.py": 7709581879446228856, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py": 8859757563354666575, - "venv/lib/python3.13/site-packages/pygments/formatters/html.py": 1918541918263449443, - "docs/api/html/group__InlineEditRequest.html": 11326341888929414464, - "venv/lib/python3.13/site-packages/numpy/doc/ufuncs.py": 11706048944901534664, - "docs/api/html/group__SupersetCompilationAdapter______init____.html": 4108260053223346394, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/METADATA": 13776251749114562660, - "docs/api/html/group__preview__response__parser.html": 10892982313411363249, - "docs/api/html/group__getUsers__Function.html": 16614665774395265088, - "docs/api/html/group__test__delete__tasks.html": 5667196877695653798, - "venv/lib/python3.13/site-packages/pip/_internal/utils/hashes.py": 17310331665429541812, - "docs/api/html/group__test__expired__event__auto__ends.html": 11366519398118405696, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_arrow_string_mixins.py": 16862663427780315543, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/indexable.py": 4131796922597075758, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_to_offset.py": 3929599794655343911, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_check_indexer.py": 2929031395357057009, - "docs/api/html/group__EnvironmentNotFoundError.html": 10410223898319526966, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/PIL/_avif.pyi": 18222325750818585549, - "docs/api/html/group__update__role.html": 15657294037902023797, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunclike.pyi": 12215374902556647369, - "venv/lib/python3.13/site-packages/PIL/PixarImagePlugin.py": 12061717157676819554, - "docs/api/html/group__DashboardHub__getValidationLabel__Function.html": 15200568549441097956, - "venv/lib/python3.13/site-packages/greenlet/TBrokenGreenlet.cpp": 10822436772777706192, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_mangle_dupes.py": 15613858733226279352, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/provision.py": 2676876852726658063, - "venv/lib/python3.13/site-packages/numpy/core/_utils.py": 14108063453670701289, - "docs/api/html/group__TestOrchestratorInsertFlow.html": 10014979661308261378, - "venv/lib/python3.13/site-packages/keyring/backend_complete.zsh": 14339032329370660432, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py": 71159121705871510, - "docs/api/html/group__llm__report__screenshot__effect__contract__Function.html": 11766872533400793722, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex_like.py": 13803156447991282315, - "docs/api/html/group__test__manifest__versioning__and__immutability.html": 7762946147479843686, - "frontend/build/_app/immutable/chunks/DG7gdutz.js": 15031800362632650272, - "backend/src/core/utils/matching.py": 1700014874878218207, - "venv/lib/python3.13/site-packages/ecdsa/ecdh.py": 12596317261561755793, - "docs/api/html/group__update__provider.html": 11827750116531988760, - "docs/api/html/group__valid__job__payload.html": 7175119685712825438, - "venv/lib/python3.13/site-packages/pygments/lexers/q.py": 15426466074881683794, - "docs/api/html/group__auth__engine.html": 14325530296654107623, - "docs/api/html/group__has__environments.html": 1097953362161131768, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_round.py": 1299086932398824299, - "docs/api/html/group__MockMappingService.html": 3005546302419221307, - "docs/api/html/group__get__db.html": 16144093670776149736, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg.py": 8023549900603235601, - "frontend/build/_app/immutable/chunks/Ck4a7ANl.js": 3561562300210641559, - "venv/lib/python3.13/site-packages/pygments/lexers/spice.py": 1376126320935115350, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/RECORD": 8971487317739420905, - "backend/tests/test_translate_history.py": 4612852312641794586, - "frontend/e2e/tests/settings.e2e.js": 1420830886812189435, - "docs/api/html/group__ensure__encryption__key.html": 10633170068816212756, - "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.py": 7658234162211356265, - "venv/lib/python3.13/site-packages/pandas/api/extensions/__init__.py": 882327406374020760, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_util.py": 16908393806993809474, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/result.py": 12262298207886620908, - "docs/api/html/group__TestGitPluginConfigIntegration.html": 5014758280841319650, - "docs/api/html/group__test__build__dataset__preview__legacy__form__data__preserves__native__filter__clauses.html": 4251865033456434354, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/base.py": 7110726987481713080, "docs/api/html/group__TasksRouter.html": 10219138142182893604, - "backend/src/schemas/translate.py": 15896789917354585618, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming_py.py": 16133454512601432815, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/WHEEL": 6514509858522675228, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_shape_base.py": 4117141349022217163, - "venv/lib/python3.13/site-packages/pygments/lexers/grammar_notation.py": 9009644754106884095, - "docs/api/html/bc_sd.png": 10097603501861382643, - "venv/lib/python3.13/site-packages/PIL/PaletteFile.py": 4104323659569809769, - "venv/lib/python3.13/site-packages/referencing/tests/test_exceptions.py": 10846225989746274213, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__test__cli__compliance__run__scaffold.html": 11653178433842340027, - "docs/api/html/group__get__dashboard__permalink__state__async.html": 14127921399924019544, - "docs/api/html/group__CotJsonFormatter.html": 14965099602834571127, - "venv/lib/python3.13/site-packages/pygments/lexers/_css_builtins.py": 17478340487186531268, - "docs/api/html/group__runBulkGitAction__Function.html": 11579106801115881689, - "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/okp_key.py": 10312614562914096039, - "docs/api/html/group__execute__scheduled__translation.html": 4589753414234722765, - "venv/lib/python3.13/site-packages/numpy/lib/introspect.pyi": 12239924839764265507, - "docs/api/html/group__test__get__profile__preferences__returns__self__payload.html": 17928297903681882891, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____startPolling.html": 5414561271979953996, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py": 2609365889369107154, - "venv/lib/python3.13/site-packages/httpcore/_async/http11.py": 7565741048880549265, - "venv/lib/python3.13/site-packages/pygments/scanner.py": 5640381644804445932, - "docs/api/html/group__LanguageDetectService.html": 5256473727119538247, - "docs/api/html/group__get__plugin__loader.html": 13012257189536947305, - "docs/api/html/group__test__basic__key.html": 14205937453274407552, - "docs/api/html/group__SupersetAuthCache.html": 17439611318511412216, - "docs/api/html/group__role__permissions.html": 12460381868773175294, - "docs/api/html/group__TaskDrawerStore.html": 6675046395365943586, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_join.py": 4213933887062089605, - "venv/lib/python3.13/site-packages/pygments/lexers/praat.py": 54226477748516386, - "venv/lib/python3.13/site-packages/pydantic/v1/json.py": 16596969681914729251, - "frontend/e2e/tests/smoke.e2e.js": 11376448043256596939, - "backend/src/models/dataset_review_pkg/_profile_models.py": 16323179255423210120, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.pyf": 1361426175133186231, - "docs/api/html/group__test__save__config__syncs__deletions__to__persistence.html": 7761700222914838538, - "backend/src/scripts/clean_release_cli.py": 777127038463156812, - "venv/lib/python3.13/site-packages/pip/_internal/cli/status_codes.py": 9143625681947330756, - "docs/api/html/group__record__clarification__feedback.html": 15926160429308910947, - "venv/lib/python3.13/site-packages/jsonschema_specifications/_core.py": 14812112544772987164, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_palettes.py": 13495896173068490516, - "docs/api/html/globals_func_p.html": 6858706269345690058, - "docs/api/html/group__CorrectionSubmitResponse.html": 6015523326141419112, - "docs/api/html/group__test__api__create__job__422__missing__name.html": 13658018012548984469, - "docs/api/html/group__create__provider.html": 15799595578960944885, - "venv/lib/python3.13/site-packages/pygments/lexers/textedit.py": 14872291021083255414, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_readers.py": 2136472634307645842, - "docs/api/html/group____parse__sqllab__result.html": 10462280637644525478, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_numpy.py": 222383250675686477, - "venv/lib/python3.13/site-packages/cryptography/__about__.py": 10471312538528457144, - "venv/lib/python3.13/site-packages/pandas/tests/test_register_accessor.py": 16661086720613729974, - "docs/api/html/group__test__orchestrator__start__session__bootstraps__recovery__state.html": 13152006356907717305, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_numpy_compat.py": 9112925138440787555, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/test_support.pyi": 3893434234020223151, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/RECORD": 10036839994791510159, - "frontend/src/lib/stores/taskDrawer.js": 16349119158364586930, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_nanfunctions.py": 2356468092336024365, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_app.py": 14788764260762890368, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/subcomponents.py": 10365594934527289233, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcrypto-2e26a48f.so.3": 3658133521245392852, - "docs/api/html/group__SystemSettings.html": 4117500366250322872, - "backend/git_repos/remote/test-repo.git/refs/heads/preprod": 2288780285329550368, - "docs/api/html/globals_b.html": 2050803601864762326, - "docs/api/html/group__extract__error__context.html": 10063584913283401746, - "docs/api/html/group__MaintenanceDashboardResult.html": 796312223553899991, - "docs/api/html/group__GitServiceModule.html": 11876529200168327141, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/__main__.py": 487896753130729554, - "docs/api/html/group__test__execution__error__handled.html": 16361858862762216227, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/__init__.py": 5863110456909134678, - "venv/lib/python3.13/site-packages/starlette/status.py": 16192064443996706569, - "venv/lib/python3.13/site-packages/pandas/core/computation/api.py": 17919983681396223380, - "frontend/build/_app/env.js": 8815854342083926790, - "venv/lib/python3.13/site-packages/jeepney/bindgen.py": 7094585345946943534, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__test__dashboards__api.html": 3569185305104571400, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/version.py": 5000350193835202949, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/assertion_session.py": 18109848200936213195, - "venv/lib/python3.13/site-packages/apscheduler/executors/twisted.py": 2695602672085615762, - "venv/lib/python3.13/site-packages/numpy/ma/extras.pyi": 15292036493781556342, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_subclass.py": 13490541871827928531, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_dropna.py": 3620959914439334511, - "frontend/src/routes/datasets/review/ReviewWorkspaceLeftSidebar.svelte": 9009994851177005358, - "backend/src/core/auth/__init__.py": 16435384828055133073, - "docs/api/html/group__SchedulerModule.html": 11350421649937027060, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/jeepney/io/threading.py": 525625939662612746, - "docs/api/html/group__test__generate__docs__empty__ids.html": 1697382171673264282, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_amd64_unix.h": 16679226724722970205, - "docs/api/html/group__test__user__role__association.html": 6316579439480183046, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_cov_corr.py": 13652339565475304679, - "frontend/src/lib/components/assistant/AssistantClarificationCard.svelte": 4264349064103133826, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reserved_words.py": 8022121780993497971, - "docs/api/html/navtreeindex6.js": 6833292869822138239, - "venv/lib/python3.13/site-packages/numpy.libs/libgfortran-040039e1-0352e75f.so.5.0.0": 13946067868274850557, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/WHEEL": 7127684561765977531, - "venv/lib/python3.13/site-packages/pandas/_version_meson.py": 4875297137037630519, - "docs/api/html/group__test__task__persistence.html": 1637708909770153120, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_masked_matrix.py": 1444179242134203344, - "docs/api/html/group____build__profile__preference__stub.html": 17148946407396504332, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/__init__.py": 9482393721453335937, - "docs/api/html/group____discover__plugin__execute__permissions.html": 14000239163315189840, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem": 15624463015628939254, - "docs/api/html/search/all_c.js": 9934344868053304351, - "docs/api/html/group__DashboardHub__doesDashboardPassColumnFilters__Function.html": 16470140080507162324, - "docs/api/html/group__selectedTask__Data.html": 6917775707953198812, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_arrow_interface.py": 9757086292358759236, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_applicator_schemas.py": 8395157526634674143, - "docs/api/html/group__BatchProcessingService.html": 10275821343943605247, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/operators.f90": 5949265961908386974, - "docs/api/html/group__get__permission__by__resource__action.html": 15000945978517431577, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log1p.csv": 16333455295913164408, - "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.py": 7352635707860427145, - "venv/lib/python3.13/site-packages/numpy/_core/multiarray.py": 4484782354376574355, - "venv/lib/python3.13/site-packages/pygments/lexers/gsql.py": 15294109558028161389, - "venv/lib/python3.13/site-packages/pip/_internal/models/index.py": 2839270935469791607, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/unused_registry.py": 10153622516112471812, - "backend/src/services/git/_url.py": 11387722385592868210, - "docs/api/html/group__CostEstimate.html": 1003350075352370602, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_count.py": 9186538441380080087, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_partial.py": 3151286293440416036, - "venv/lib/python3.13/site-packages/pandas/io/clipboard/__init__.py": 3656451154829206939, - "docs/api/html/group__DashboardsApi.html": 8580056325448816200, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.py": 4440659352851541547, - "venv/lib/python3.13/site-packages/pandas/core/groupby/ops.py": 2036968385544457016, - "docs/api/html/group__PromoteRequest.html": 11374219910371019333, - "docs/api/html/group__handleCommit__Function.html": 15953199697396659260, - "venv/lib/python3.13/site-packages/starlette/_utils.py": 8384087194580626069, - "docs/api/html/group__subscribe__logs.html": 16463697240897933473, - "docs/api/html/group__LLMAnalysisPackage.html": 5388615837684236664, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/resource_protector.py": 14354891358060085360, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/_mapping.py": 11752660241391232497, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/twodim_base.pyi": 3773329770170248877, - "docs/api/html/group__test__git__service__get__repo__path__recreates__base__dir.html": 7635897550275628246, - "docs/api/html/group__AsyncAPIClient__aclose.html": 207928245913411569, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get_value.py": 389530025760539462, - "docs/api/html/group__create__dashboard__export.html": 1314555688976481845, - "venv/lib/python3.13/site-packages/more_itertools/__init__.pyi": 16479506520351014608, - "venv/lib/python3.13/site-packages/pandas/tests/construction/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numeric.py": 5627177940393829333, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategy_options.py": 16342048970907383117, - "docs/api/html/group__run__artifact__import.html": 14280186280620385233, - "docs/api/html/tab_ad.png": 18360990721051584886, - "docs/api/html/group__ValidationTaskConfig.html": 9838515327973111291, - "venv/lib/python3.13/site-packages/pillow.libs/libXau-154567c4.so.6.0.0": 4340972632724893052, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__reason.html": 7755071372929183522, - "backend/git_repos/remote/test-repo.git/hooks/commit-msg.sample": 13435789416588783681, - "docs/api/html/group__get__run__records.html": 5134890805919610739, - "docs/api/html/group__handleBulkPush__Function.html": 5303517562095146306, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_repr.py": 13147587049887075335, - "docs/api/html/group__DashboardHub__clearColumnFilter__Function.html": 11839612735463468101, - "venv/lib/python3.13/site-packages/numpy/lib/_iotools.py": 10522826373041592861, - "frontend/src/routes/settings/StorageSettings.svelte": 1953157077500445570, - "frontend/src/components/__tests__/task_log_viewer.test.js": 4344163242946147110, - "venv/lib/python3.13/site-packages/websockets/exceptions.py": 12048063047468402740, - "docs/api/html/group__MarkerLogger__reason.html": 11360973389497670827, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_extras.py": 16160090335191788104, - "docs/api/html/group__SupersetClientGetDatabaseByUuid.html": 12211338617460141726, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/padding.py": 13763154154862871596, - "docs/api/html/group__TaskList.html": 17909740066705680833, - "docs/api/html/search/groups_8.js": 9394148223818886948, - "docs/api/html/group__SearchPluginModule.html": 11654743945648339497, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo77.f": 4046503346951676582, - "docs/api/html/group__frontend__src__services__adminService__js____getRoles.html": 5558475973207051005, - "venv/lib/python3.13/site-packages/pandas/io/excel/__init__.py": 6662687963241471393, - "docs/api/html/group__DashboardActionRoutes.html": 7575118008410498480, - "venv/lib/python3.13/site-packages/numpy/_core/tests/_locales.py": 18178500267053825979, - "venv/lib/python3.13/site-packages/pygments/lexers/sql.py": 15566262873151658034, - "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.pyi": 6796322258258182775, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/licenses/LICENSE": 16147999986886816451, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/ssh.py": 16262079536832961001, - "backend/src/api/routes/maintenance/_router.py": 1368406181799456173, - "venv/lib/python3.13/site-packages/pygments/lexers/make.py": 5345282370426217663, - "venv/lib/python3.13/site-packages/pygments/lexers/html.py": 17288963642862550116, - "docs/api/html/group__run__translation.html": 12662554478381074393, - "docs/api/html/group__resolve__Function.html": 442849213114832901, - "docs/api/html/group__test__manifest__service__rebuild__creates__new__version.html": 15569805611870644295, - "docs/api/html/group__health__store__Store.html": 8056741375988649819, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/RECORD": 1689342214025776630, - "docs/api/html/group____build__default__preference.html": 12609186263613185575, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/token_validator.py": 13917237710752731397, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/char.pyi": 13143560686049627445, + "venv/lib/python3.13/site-packages/_pytest/assertion/util.py": 16957445183087315124, + "docs/api/html/group__get__api__key__principal.html": 5141844013262669360, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_copy.py": 12902342280706372953, + "venv/lib/python3.13/site-packages/numpy/lib/user_array.pyi": 1968484998382771986, + "venv/lib/python3.13/site-packages/pygments/lexers/dylan.py": 16246299896150112270, "venv/lib/python3.13/site-packages/pandas/tests/computation/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/METADATA": 2624107638694809013, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/_orm_constructors.py": 4276726508722638048, - "frontend/src/components/DashboardGrid.svelte": 3453508702214419189, - "venv/lib/python3.13/site-packages/jeepney/io/tests/conftest.py": 13381752264278264218, - "docs/api/html/group__createRole__Function.html": 17635275329141621796, - "docs/api/html/group__NoExternalEndpointsStage.html": 18436422189478912804, - "docs/api/html/group__CleanPolicyEngine.html": 5866910644172522724, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dh.pyi": 3254633032236269842, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_regression.py": 9375525053479844400, - "venv/lib/python3.13/site-packages/dateutil/tz/win.py": 8427821225251397364, - "venv/lib/python3.13/site-packages/PIL/ImageShow.py": 11437096951764124817, - "venv/lib/python3.13/site-packages/pygments/lexers/d.py": 18120801227801223376, - "docs/api/html/group__PublicationRecord.html": 12219741124071026101, - "docs/api/html/group__handleDrop__Function.html": 7991568720228574649, - "venv/lib/python3.13/site-packages/cffi/commontypes.py": 11198107299826138465, - "venv/lib/python3.13/site-packages/uvicorn/logging.py": 12858188863836778374, - "docs/api/html/group__frontend__src__types__dashboard__ts____DashboardMetadata.html": 16596283258366470252, - "venv/lib/python3.13/site-packages/pyasn1/codec/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/idnadata.py": 1169745920682458213, - "venv/lib/python3.13/site-packages/httpx/_urlparse.py": 8270294590945652412, - "venv/lib/python3.13/site-packages/pygments/styles/algol_nu.py": 6229131016121450375, - "backend/src/core/config_models.py": 4102600119446697065, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/py.typed": 15130871412783076140, - "docs/api/html/group__SupersetCompilationAdapter____request__sql__lab__session.html": 2880186868861912390, - "venv/lib/python3.13/site-packages/numpy/char/__init__.pyi": 12214207343146244743, - "docs/api/html/group__MaintenanceDashboardBanner.html": 13827348584356629256, - "venv/lib/python3.13/site-packages/numpy/_typing/_extended_precision.py": 4201820494689784136, - "venv/lib/python3.13/site-packages/jeepney/io/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/_libs/join.pyi": 369262334342439364, - "venv/lib/python3.13/site-packages/pandas/tests/api/test_api.py": 13385690673368400635, - "docs/api/html/group__DatasetReviewSessionStoreTests__Module.html": 5623361748252231964, - "venv/lib/python3.13/site-packages/pygments/lexers/_mysql_builtins.py": 16087227298679432006, - "frontend/src/routes/profile/__tests__/profile-settings-state.integration.test.js": 5982682724976240528, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawerForTaskIfPreferred.html": 7335878052514362026, - "venv/lib/python3.13/site-packages/jeepney/low_level.py": 6546206679858571608, - "frontend/src/routes/+layout.svelte": 729110825767878269, - "docs/api/html/group__startup__event.html": 11110363748286248660, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_indexing.py": 5063545640498065297, - "docs/api/html/group__TestReportsDetailApi.html": 1827756407896142735, - "backend/src/api/routes/profile.py": 8666418382456481533, + "venv/lib/python3.13/site-packages/pygments/lexers/testing.py": 17148598391210353576, + "docs/api/html/group__Logger.html": 11994362780557213627, + "docs/api/html/globals_func_g.html": 530559393513453799, + "venv/lib/python3.13/site-packages/pandas/io/json/__init__.py": 12333505155070172626, + "docs/api/html/group__delete__validation__policy.html": 7423748920813803796, + "venv/lib/python3.13/site-packages/httpcore/_backends/__init__.py": 15130871412783076140, + "docs/api/html/group__test__us3__launch__endpoint__requires__launch__permission.html": 13518476876564276937, + "docs/api/html/group____resolve__dashboard__id__from__ref__async.html": 5782530713734199781, + "venv/lib/python3.13/site-packages/pandas/errors/cow.py": 14381282473643980776, + "frontend/src/routes/dashboards/+page.svelte": 5522148689707175338, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/__init__.py": 16146480198665866916, + "docs/api/html/group__DatasetReviewUs2WorkspaceUxTests__Module.html": 7172401692085262664, + "docs/api/html/group__ImportedFilter.html": 10182695392168978110, + "venv/lib/python3.13/site-packages/cryptography/x509/name.py": 15338066443257445241, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/oracledb.py": 7158791242292470472, + "docs/api/html/group__GitModels.html": 8608208250530048906, + "docs/api/html/group__execute.html": 813912290626657753, "backend/src/core/timezone.py": 15725829269123183582, - "docs/api/html/group__test__database__url__raises__when__both__unset.html": 842987545358384743, - "docs/api/html/group__list__ad__mappings.html": 4036472726252673441, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_monotonic.py": 11817523305776951162, - "docs/api/html/group__test__reload__validation__policy__removes__job__when__policy__gone.html": 2311098519001920581, - "docs/api/html/group____iter__route__files.html": 11857371736918099276, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/RECORD": 3288669175908898843, - "venv/lib/python3.13/site-packages/pip/_internal/commands/cache.py": 1892313355296776523, - "docs/api/html/group__TaskManagerModule.html": 10905637838927544085, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_arrayobject.py": 9578555381494297966, - "venv/lib/python3.13/site-packages/fastapi/requests.py": 14587947416691824903, - "docs/api/html/group__BatchApproveSemanticItemRequest.html": 8441557849246446003, - "venv/lib/python3.13/site-packages/numpy/tests/test_reloading.py": 17532116685329953400, - "venv/lib/python3.13/site-packages/pandas/_libs/arrays.cpython-313-x86_64-linux-gnu.so": 8634234308863090562, - "venv/lib/python3.13/site-packages/pygments/lexers/sgf.py": 6802546617450210439, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/relationships.py": 8401569216905291119, - "venv/lib/python3.13/site-packages/pandas/tests/extension/date/__init__.py": 16789767433439616593, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py": 3531093454660385247, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayobject.h": 4345432890284821918, - "docs/api/html/group__find__affected__dashboards.html": 15893909015363041840, - "docs/api/html/group__test__error__string__fallback.html": 16246095716833508917, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_repeat.py": 18394463352314692642, - "docs/api/html/group__ImportedFilterDto.html": 5501399725886626676, - "backend/git_repos/remote/test-repo.git/objects/4e/227f4d8289ea657ae813b1460f979ae4b676b3": 5659284628452785331, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildImportMilestones.html": 8235208288594735263, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/warnings_and_errors.pyi": 9019577563960071498, - "docs/api/html/group__RegisterCandidateRequest.html": 7180845738222833015, - "docs/api/html/group__normalize__answer__value.html": 5431564849732382081, - "docs/api/html/group__SupersetClientGetDashboardDetail.html": 4781513685980981790, - "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.cpython-313-x86_64-linux-gnu.so": 3478629877770507853, - "frontend/src/routes/settings/settings-utils.js": 1940619437304867415, - "venv/lib/python3.13/site-packages/pygments/styles/paraiso_dark.py": 3976779833379189837, - "docs/api/html/group__MaintenanceStartResponse.html": 16246400810680453876, - "docs/api/html/group____extract__result__deep__links.html": 2321729088905700625, - "venv/lib/python3.13/site-packages/pydantic_settings/utils.py": 14396350455520958703, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/npyio.pyi": 15811156298637131744, - "docs/api/html/group__ReadinessState.html": 18424054850820487060, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_deprecations.py": 14624516389510242394, - "docs/api/html/group__DatasetReviewClarificationModels.html": 4185914668418818885, - "docs/api/html/group__TaskGraphModule.html": 1612918400039070240, - "venv/lib/python3.13/site-packages/_pytest/nodes.py": 515939386237558331, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period.py": 9715843209581883915, - "venv/lib/python3.13/site-packages/dateutil/_common.py": 12492546559232079071, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/x509.pyi": 17458894097215345719, - "docs/api/html/group____FakeConfigManager.html": 11213537801456913525, - "docs/api/html/group__test__sync__environment__requires__existing__env.html": 7580953010735906417, - "docs/api/html/group____delete__stale__environment__records.html": 1017402888179208355, - "frontend/src/routes/datasets/__tests__/ColumnsTable.test.js": 15883951776898228284, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/anyio/streams/buffered.py": 8916188172110578587, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/array.py": 12658302948819867972, - "backend/git_repos/remote/test-repo.git/objects/8c/4ea7958b7a7e140d27e1e7d8d1f9912fdf5992": 5862166356451798197, - "backend/src/services/clean_release/preparation_service.py": 9259410398708054305, - "docs/api/html/group__mask__pii__value.html": 9015692423384438989, - "venv/lib/python3.13/site-packages/pandas/_libs/indexing.cpython-313-x86_64-linux-gnu.so": 13188040757764159731, - "docs/api/html/group__handleEdit__Function.html": 15858142229042020143, - "venv/lib/python3.13/site-packages/pandas/_libs/pandas_datetime.cpython-313-x86_64-linux-gnu.so": 15874094681262240628, - "venv/lib/python3.13/site-packages/keyring/testing/util.py": 11195868112805409554, - "backend/src/plugins/translate/dictionary_import_export.py": 12639118203353044308, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_date_range.py": 16128552513324017349, - "docs/api/html/group__SemanticFieldEntryDto.html": 7770402299955438341, - "docs/api/html/search/functions_c.js": 2394026458529255260, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply_relabeling.py": 3930092097678339743, - "backend/src/api/routes/translate/_run_routes.py": 16216908966766661647, - "venv/lib/python3.13/site-packages/websockets/__init__.py": 4916307256304054040, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/connection.py": 5582413226288987808, - "venv/lib/python3.13/site-packages/pygments/lexers/minecraft.py": 10133579583270214110, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py": 853808724844451770, - "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.cpython-313-x86_64-linux-gnu.so": 16327106317503267067, - "venv/lib/python3.13/site-packages/numpy/_array_api_info.py": 9709101823494959741, + "venv/lib/python3.13/site-packages/pip/_internal/commands/install.py": 17631777189475433503, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_ufunc.py": 5025605639221470838, + "docs/api/html/group__get__dashboards__page__async.html": 17651441154400395496, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/jws_eddsa.py": 833927568269372333, + "docs/api/html/group__test__logger.html": 11504400505756254792, + "docs/api/html/group__RepoStatusBatchResponse.html": 16247460233048160196, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/errors.py": 16711622442922413520, + "venv/lib/python3.13/site-packages/pip/_internal/cli/__init__.py": 11914508471759255927, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_errors.py": 3301889847545579393, + "docs/api/html/group__test__endpoints__require__authentication.html": 6923064043031748125, + "venv/bin/pyrsa-keygen": 8222542582170363672, + "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/video.webm": 6248292794123488182, + "docs/api/html/group__test__extract__objects__from__zip__collects__all__types.html": 627526279226989787, + "venv/lib/python3.13/site-packages/fastapi/security/http.py": 8465565118555441408, + "docs/api/html/group__test__get__task__logs__invalid__limit.html": 7729306372384942790, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine.py": 16193618152415684421, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/__init__.py": 9997757256282720103, + "frontend/src/routes/dashboards/__tests__/dashboard-profile-override.integration.test.js": 4930404579577741926, + "venv/lib/python3.13/site-packages/greenlet/TPythonState.cpp": 12828594265749606443, + "docs/api/html/group__HealthSummaryResponse.html": 15269424363939168020, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/__init__.py": 18069619555394133945, + "docs/api/html/group__internal__sources__only.html": 7674825160661281445, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_comparison.py": 11333735187162513512, + "docs/api/html/group__SemanticSourceResolver.html": 9852035711802845664, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5support-d0bcff84.so.0.1": 16819924515291719993, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexer.py": 14400766876373764268, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_data.py": 2397937327057341, + "docs/api/html/group__cancel__operation.html": 16696836689617631015, + "docs/api/html/group__locale__Store.html": 3681806005291514520, + "docs/api/html/group__loadGiteaRepos__Function.html": 6310672505465620281, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/LICENSE": 4672913476475760762, + "docs/api/html/group__I18nModule.html": 3811966300603139836, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_npfuncs.py": 13987318550267921389, + "docs/api/html/group__build__violation.html": 1522726666566321384, + "venv/lib/python3.13/site-packages/pandas/_version.py": 3480474785419232091, + "docs/api/html/group__get__run__records.html": 5134890805919610739, + "docs/api/html/group__AssistantDatasetReview.html": 12183015005739052037, + "docs/api/html/search/functions_a.js": 12926479784540505760, + "venv/lib/python3.13/site-packages/PIL/SpiderImagePlugin.py": 12234762087257163606, + "venv/lib/python3.13/site-packages/pygments/formatters/groff.py": 6480830447984073574, + "venv/lib/python3.13/site-packages/passlib/handlers/postgres.py": 14162382430051693313, + "venv/lib/python3.13/site-packages/pygments/lexers/_stata_builtins.py": 18053452765493556047, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_install.py": 7181005402818830655, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_utils.py": 2329717399120006435, + "venv/lib/python3.13/site-packages/httpcore/_async/connection.py": 2155744160496556069, + "backend/src/core/logger/__tests__/test_logger.py": 7352396322555571031, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_frequencies.py": 14039315221824123200, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/twodim_base.pyi": 10156504423258357540, + "venv/lib/python3.13/site-packages/attr/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_struct_accessor.py": 6567451952835390075, + "venv/lib/python3.13/site-packages/attrs/setters.py": 5775359015245600592, + "docs/api/html/group__test__full__mode__default.html": 3915571983694234061, + "venv/lib/python3.13/site-packages/numpy/core/overrides.pyi": 1968648615494204783, + "venv/lib/python3.13/site-packages/pydantic/env_settings.py": 18250976255197243083, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/engines.py": 10876954465293959720, + "docs/api/html/search/functions_4.js": 10455355681176457663, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.cpython-313-x86_64-linux-gnu.so": 18139875206371157315, + "docs/api/html/group__test__invalid__add__log__fn.html": 13488633198272059949, + "docs/api/html/group__BatchInsertService.html": 3576868243265659369, + "venv/lib/python3.13/site-packages/authlib/jose/jwk.py": 13560749074922587626, + "venv/lib/python3.13/site-packages/pip/_internal/commands/index.py": 16715578868094112894, + "docs/api/html/group__test__end__returns__202.html": 3291889015034069048, + "venv/lib/python3.13/site-packages/anyio/streams/memory.py": 11762722143503428823, + "docs/api/html/group__LLMProviderConfig.html": 477820453193814610, + "venv/lib/python3.13/site-packages/pip/_internal/network/cache.py": 10606522397693824641, + "docs/api/html/group____get__default__tz__name.html": 1065268266892012697, + "docs/api/html/group__buildAssistantSeedMessage__Function.html": 8962217409153936203, + "docs/api/html/group__TestSessionConfig.html": 10172148335639053015, + "docs/api/html/group____await__none.html": 14771125371408463258, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/errors.py": 12935154705443176852, + "venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py": 13594639612360346306, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/__init__.py": 18019562641348687547, + "venv/lib/python3.13/site-packages/_pytest/skipping.py": 10561585274103209115, + "docs/api/html/group__execute__scheduled__translation.html": 4589753414234722765, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo77.f": 12000068001851270396, + "venv/lib/python3.13/site-packages/pandas/_libs/interval.pyi": 15698173755300703836, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_setitem.py": 15731267129055947469, + "venv/lib/python3.13/site-packages/pandas/io/excel/_pyxlsb.py": 10035587889954731645, + "venv/lib/python3.13/site-packages/pandas/io/excel/_calamine.py": 13354427767660487637, + "venv/lib/python3.13/site-packages/pygments/lexers/pawn.py": 18127303119414402594, + "backend/src/models/translate.py": 3611792946430568934, + "backend/src/services/notifications/service.py": 3120728755335117444, + "venv/lib/python3.13/site-packages/pandas/api/typing/aliases.py": 11194270368147942988, + "backend/git_repos/remote/test-repo.git/objects/a7/49f11fad695dff77faf1708c7320867d0be1df": 4641609158325033031, + "docs/api/html/group__SupersetClientValidateImportFile.html": 9535308086571101211, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pyi": 15790738442952696034, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/METADATA": 14608873187265321271, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_logical.py": 6242457515917145500, + "docs/api/html/globals_u.html": 9479390597620491497, + "frontend/src/routes/maintenance/+page.svelte": 5467232163447861441, + "docs/api/html/group__score__risks.html": 16643542131846980434, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/unused_registry.py": 10153622516112471812, + "docs/api/html/group__dataset__review__api__dependencies.html": 4669339577033010960, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/sdist.py": 9490366272695301385, + "frontend/src/routes/settings/LoggingSettings.svelte": 5172094246995863925, + "venv/lib/python3.13/site-packages/pygments/lexers/graph.py": 10445044822847205061, + "docs/api/html/group__deleteTask__Function.html": 4139589046906492186, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/multiprocess.py": 10047012560534876149, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/licenses/LICENSE": 13322127602069971876, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/__init__.py": 15130871412783076140, + "docs/api/html/group__Base.html": 12380905760850821737, + "docs/api/html/group__TestAuth.html": 14086643015610504609, + "docs/api/html/group__ExportArtifact.html": 1743575886963594890, + "venv/lib/python3.13/site-packages/ecdsa/ssh.py": 17010933326090778158, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/color.py": 15062855556511243610, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/pyinstaller-smoke.py": 13570990531070630950, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_interaction.py": 12216477658994324787, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_to_numpy.py": 14845850776625255794, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_numpy.py": 9825804096508014886, + "venv/lib/python3.13/site-packages/pygments/filters/__init__.py": 2992007913496948339, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/roles.py": 5448455900674232810, + "docs/api/html/group__HealthCenterPage.html": 18169365959978788379, + "docs/api/html/group__test__list__runs__filter__job__id.html": 7845039968622069882, + "docs/api/html/group__test__revoke__valid.html": 15486457788430868004, + "docs/api/html/group__save__config.html": 12986586572590781292, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/yaml.py": 17801117405950663611, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_symbolic.py": 8093576026032854125, + "docs/api/html/group__ApprovalState.html": 16834628662566561513, + "docs/api/html/group__test__test__git__config__fails__validation.html": 14329115310381033327, + "venv/lib/python3.13/site-packages/PIL/_imaging.cpython-313-x86_64-linux-gnu.so": 1425926264271197601, + "venv/lib/python3.13/site-packages/pytest_asyncio/__init__.py": 8162213292669110211, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rank.py": 14115215895836854973, + "docs/api/html/group__AppModule.html": 7571679846299956443, + "docs/api/html/group__TranslationSchedule.html": 9479348771909724895, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_is_homogeneous_dtype.py": 15378147393056707851, + "docs/api/html/group__SessionEventPayload.html": 17617366798998251944, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_localize.py": 14495969595744172955, + "docs/api/html/group____ensure__conversation.html": 4799209468607073839, + "docs/api/html/group__lock__field__semantic.html": 2797198914997806470, + "venv/lib/python3.13/site-packages/_pytest/legacypath.py": 7512065138259788426, + "docs/api/html/group__MaintenanceBannerPage.html": 9819977167182180539, + "venv/bin/pyrsa-sign": 15564653706771700451, + "frontend/src/routes/+page.ts": 11160596731849880920, + "docs/api/html/group__test__no__matching__dashboards.html": 4286276093696893337, + "venv/lib/python3.13/site-packages/httpcore/_backends/anyio.py": 6167729964577005634, + "docs/api/html/group__test__api__error.html": 8524046232458013902, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/token_validator.py": 13917237710752731397, + "docs/api/html/group__FieldProvenance.html": 13226719866506423057, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/RECORD": 9964194961227523130, + "venv/lib/python3.13/site-packages/starlette/background.py": 10232175205397808417, + "docs/api/html/group____collect__yaml__objects.html": 706456919847215611, + "venv/lib/python3.13/site-packages/attr/_make.py": 2348458829123990579, + "venv/lib/python3.13/site-packages/fastapi/_compat/v1.py": 9776181748061371513, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_interval.py": 14749388246573356959, + "venv/lib/python3.13/site-packages/pandas/io/formats/string.py": 14718123345899686696, + "docs/api/html/group__frontend__src__lib__api__validation__js____fetchRuns.html": 486975390462306986, + "venv/lib/python3.13/site-packages/multipart/decoders.py": 11346752866822986212, + "docs/api/html/group__set__trace__id.html": 10194237527174132581, + "backend/src/schemas/health.py": 17351928434071690723, + "backend/src/api/routes/__tests__/test_assistant_authz.py": 16737581719292587151, + "venv/lib/python3.13/site-packages/git/refs/remote.py": 4391983355222612205, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_convert_dtypes.py": 5005259275646154894, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/encoder.py": 3261739474564948330, + "docs/api/html/group__setToken__Function.html": 17680187825961724392, + "docs/api/html/group__test__basic__key.html": 14205937453274407552, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/wrapper.py": 4633931060151989960, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/base.py": 13634605726041717070, + "frontend/src/routes/translate/history/+page.svelte": 9504117584329454876, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_tools.py": 13994164937603424236, + "docs/api/html/group__test__tui__non__tty__refuses__startup.html": 11290435043577300064, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/requests.py": 6703055041957748929, + "docs/api/html/group__test__load__config__syncs__environment__records__from__existing__db__payload.html": 5104582144704294884, + "venv/lib/python3.13/site-packages/apscheduler/triggers/combining.py": 1264860868414723829, + "venv/lib/python3.13/site-packages/pygments/lexers/gsql.py": 15294109558028161389, + "docs/api/html/group____get__git__config__or__404.html": 9100308666057061844, + "docker/backend.entrypoint.sh": 7511885445313737722, + "docs/api/html/group__test__save__config__syncs__environment__records__for__fk__backed__flows.html": 13497171163902546222, + "docs/api/html/group__TranslateDictionaryRoutesModule.html": 3644389177415562158, + "backend/src/api/routes/git/_helpers.py": 17217408354237437064, + "docs/api/html/group__toasts__Data.html": 4779111389094535460, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_format.py": 9331389900588353667, + "backend/src/plugins/translate/__tests__/test_orthogonal_fixes.py": 14006447443227908509, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py": 4111797647365870137, + "venv/lib/python3.13/site-packages/itsdangerous/encoding.py": 205863946173486958, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/__init__.py": 11808051681178127650, + "venv/lib/python3.13/site-packages/git/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/DcxImagePlugin.py": 11767274218348117481, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath.py": 13010939919217862164, + "docs/api/html/group__ReportsRouter.html": 16841333188170208308, + "venv/lib/python3.13/site-packages/websockets/connection.py": 5168795926684067030, + "docs/api/html/group__DatasetReviewExecutionModels.html": 15585823821459317231, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_replace.py": 8784361132920473760, + "venv/lib/python3.13/site-packages/apscheduler/job.py": 1858376671986108667, + "venv/lib/python3.13/site-packages/pip/_internal/utils/direct_url_helpers.py": 6075890513530296650, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/serializer.py": 8708767945343850816, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/cli.py": 6382193953727339271, + "venv/lib/python3.13/site-packages/pygments/lexers/_lua_builtins.py": 12653747099659257203, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/licenses/COPYING": 14459782230785388765, + "dist/docker/ss-tools-enterprise-0.1.7.bundle": 1014694959540897198, + "docs/api/html/group__get__environment__databases.html": 8137104331651503447, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py": 5995058808539530576, + "docs/api/html/group__test__candidate__lifecycle__transitions.html": 12801528720010351292, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_fsspec.py": 6936167344144998172, + "venv/lib/python3.13/site-packages/passlib/utils/pbkdf2.py": 15802959653228956404, + "venv/lib/python3.13/site-packages/pandas/io/sas/sasreader.py": 12766780210114366112, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/__init__.py": 260774374497653876, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/providers.py": 7267821734584612032, + "venv/lib/python3.13/site-packages/git/refs/log.py": 5175801243332619268, + "docs/api/html/group__build__context__keys.html": 16614569594821867197, + "docs/api/html/group__DatabaseMapping.html": 6106397605843909678, + "docs/api/html/group__get__mappings.html": 9283879926950218053, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_pairwise.py": 2673370322448160842, + "docs/api/html/group__test__sync__environment__skips__resources__without__uuid.html": 6431696107355961195, + "venv/lib/python3.13/site-packages/pydantic/parse.py": 13750341403702794392, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/mypy.py": 823265492261272631, + "docs/api/html/group__DebugPluginModule.html": 18319951556663504137, + "docs/api/html/group__getStatusLabel__Function.html": 4868893043616825247, + "docs/api/html/group__test__build__dataset__preview__legacy__form__data__preserves__native__filter__clauses.html": 4251865033456434354, + "venv/lib/python3.13/site-packages/pygments/lexers/jsx.py": 13855228003748707341, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/licenses/LICENSE": 6965457853328313914, + "docs/api/html/group__TranslationLanguage.html": 10040359770577893237, + "backend/tests/services/clean_release/test_policy_resolution_service.py": 1385527700120144230, + "docs/api/html/group__test__start__returns__202.html": 4326452274740298704, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/meson.build": 9642189398934130247, + "venv/lib/python3.13/site-packages/pandas/core/base.py": 10262717073058743378, + "frontend/src/lib/components/layout/TopNavbar.svelte": 16154704878262712977, + "venv/lib/python3.13/site-packages/pydantic/error_wrappers.py": 11273827025986230629, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh27697.f90": 4990763135487995517, + "docs/api/html/group__loadFiles__Function.html": 12100105929385234165, + "backend/src/api/routes/translate/_helpers.py": 6946167155296110158, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/base.py": 16052592982063844259, + "docs/api/html/group__MaintenanceStore__state.html": 4681812142330433679, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_index.py": 12015738970771703312, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/util.py": 6088868370298560146, + "docs/api/html/group____serialize__dataset__review__context.html": 10048110432135787617, + "docs/api/html/group__test__create__gitea__pull__request__retries__with__remote__host__on__404.html": 16699153216493033230, + "frontend/build/_app/immutable/chunks/BQClHoVV.js": 12035558556234883558, + "backend/src/core/utils/superset_context_extractor/_recovery.py": 11393453671559067169, + "docs/api/html/group__ASSISTANT__AUDIT.html": 13579278608656856819, + "backend/src/core/config_models.py": 4102600119446697065, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/cookies.py": 4997226307650350876, + "docs/api/html/group__test__storage__settings__endpoint__shape.html": 7615304938869259368, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_editable.py": 14352954849821931255, + "docs/api/html/group__PROVIDER__DEFAULTS.html": 865392888129938339, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py": 10505346210114745037, + "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.py": 3646474165295296669, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-expm1.csv": 8710594949902856122, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_iteration.py": 15247340610412364508, + "docs/api/html/group__prepare__candidate__endpoint.html": 8691475225668238751, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py": 8659210689723088321, + "backend/src/api/routes/git/_config_routes.py": 9064062564638523731, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimelike.py": 5317270578347271174, + "venv/lib/python3.13/site-packages/numpy/ma/mrecords.py": 687170905107114309, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/common.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/_libs/parsers.cpython-313-x86_64-linux-gnu.so": 14134106903573638108, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64_np126.pkl.gz": 1630203486813146863, + "docs/api/html/group__test__extract__native__filters__from__key__dict__value.html": 1412362209295645370, + "venv/lib/python3.13/site-packages/passlib/handlers/sun_md5_crypt.py": 1949993216707182407, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py": 3515468590005709183, + "docs/api/html/group__test__calculate__schedule__zero__window.html": 14555909111709212057, + "docs/api/html/group__list__runs.html": 16735737419042964016, + "docs/api/html/group__setAssistantDatasetReviewSessionId__Function.html": 9358397705824505617, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/WHEEL": 7145482834744213753, + "venv/lib/python3.13/site-packages/websockets/client.py": 5229500079651465772, + "docs/api/html/group__CompiledPreviewDto.html": 13008981899007160436, + "venv/lib/python3.13/site-packages/jsonschema/tests/_suite.py": 6307666366417438489, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/_mapping.py": 13335227913146542481, + "docs/api/html/group__preview__response__parser.html": 10892982313411363249, + "docs/api/html/group__TestGetReportsAsync__Class.html": 7630216116279114545, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_drop.py": 11341278355685583133, + "backend/src/plugins/translate/preview_session_serializer.py": 8901687711861433007, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/WHEEL": 2357997949040430835, + "docs/api/html/group__docker__frontend__Dockerfile.html": 17852951405421150533, + "docs/api/html/group__TestTranslateExecutorFilter.html": 7954474206063805341, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/RECORD": 13494296690334916257, + "docs/api/html/group__ProfilePreference.html": 684032724215963462, + "docs/api/html/group__INITIAL__PERMISSIONS.html": 5081007697300821170, + "docs/api/html/group__ConfigModels.html": 17708399333384858246, + "docs/api/html/group__test__api__key__auth__environment__scope__ok.html": 15499030741410944952, + "docs/api/html/group__invalidateRepositoryStatuses__Function.html": 3906405379094094913, + "venv/lib/python3.13/site-packages/pandas/tests/apply/conftest.py": 13798335354961261346, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/WHEEL": 1598805043198466373, + "venv/lib/python3.13/site-packages/pygments/lexers/lisp.py": 11872331622183590758, + "docs/api/html/globals_func_l.html": 2863216576558791276, + "docs/api/html/group____FakeTaskManager.html": 3818898705831684798, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_dtypes.py": 481191723198819568, + "docs/api/html/group__closeAssistantChat__Function.html": 5957962497908010406, + "venv/lib/python3.13/site-packages/pygments/lexers/felix.py": 2334148799642808111, + "docs/api/html/group__test__end__not__found.html": 15644048943441130439, + "docs/api/html/group__onDestroy__Function.html": 1351684727316654046, + "frontend/src/services/__tests__/gitService.test.js": 828032813171400202, + "backend/src/api/routes/dataset_review.py": 6955521302681155464, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/licenses/LICENSE.rst": 5960743202700406649, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/WHEEL": 18203500019759199992, + "venv/lib/python3.13/site-packages/apscheduler/triggers/interval.py": 3964550092873794670, + "venv/lib/python3.13/site-packages/pydantic/aliases.py": 14481980467204711659, + "backend/src/plugins/translate/preview_review.py": 12164339938653635656, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py": 15377043158761455858, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_isoc.py": 4923640657296076239, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_split_partition.py": 2715491071550545667, + "docs/api/html/group__test__get__dialect__from__database.html": 18388157090569434819, + "docs/api/html/group__remove__chart__from__layout.html": 9632071695913277477, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_size.py": 12449835577388069407, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ctypeslib.pyi": 8623725566434725526, + "docs/api/html/group__test__save__config__syncs__deletions__to__persistence.html": 7761700222914838538, "docs/api/html/globals_func_w.html": 13805272721469242053, - "venv/lib/python3.13/site-packages/numpy/testing/tests/test_utils.py": 5291132432383518704, + "docs/api/html/group__CreateAdminScript.html": 9770308413681571026, + "docs/api/html/group__test__api__datasource__columns__missing__env.html": 12699662960552384887, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcom_err-2abe824b.so.2.1": 12870528025628871295, + "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.pyi": 12052895927933479005, + "venv/lib/python3.13/site-packages/pandas/core/internals/blocks.py": 6448406236246476333, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_table.tpl": 8706980975714184465, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/WHEEL": 16223367184831500348, + "venv/lib/python3.13/site-packages/urllib3/util/util.py": 12490259045660543460, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/_argcomplete.py": 3586514420674089892, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.pyf": 4660465266594480405, + "venv/lib/python3.13/site-packages/pandas/core/interchange/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows.py": 13341168721223961872, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_validators.py": 14178329676643700151, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hashes.py": 8545380307201732345, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine.py": 4446011286411407708, + "docs/api/html/group__test__abort__merge__calls__service__and__returns__result.html": 12531578259821930099, + "docs/api/html/group__test__get__job.html": 17537338432908316746, + "docs/api/html/group__get__dataset__detail.html": 14957609761194222972, + "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.py": 738588649412338185, + "venv/lib/python3.13/site-packages/passlib/win32.py": 5311393450489898748, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_compat.py": 4677414058216516380, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_cte.py": 9274103293201050794, + "backend/src/api/routes/assistant/_llm_planner_intent.py": 11839125877947166245, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/selectable.py": 14646717349365560549, + "venv/lib/python3.13/site-packages/PIL/_imagingcms.cpython-313-x86_64-linux-gnu.so": 15198265718775753589, + "docs/api/html/group__getTask__Function.html": 11413172980831416188, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/__init__.py": 11850945878149145990, + "docs/api/html/group__AutomationPage.html": 6103305575315188006, + "docs/api/html/group____parse__cron.html": 6292735699445558304, + "docs/api/html/globals_dup.js": 12007683462695373588, + "venv/lib/python3.13/site-packages/pydantic/main.py": 14086938579850319884, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/endpoint.py": 18260428408051728802, + "backend/src/services/__tests__/test_rbac_permission_catalog.py": 2963458864311994307, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_legend.py": 13094583566764796297, + "venv/lib/python3.13/site-packages/smmap/mman.py": 9091562788592406032, + "backend/src/services/clean_release/manifest_service.py": 7248530010607024664, + "venv/lib/python3.13/site-packages/websockets/cli.py": 14049106025886056110, + "docs/api/html/group__DashboardHub__getVisibleFilterOptions__Function.html": 17131127528619784346, + "venv/lib/python3.13/site-packages/passlib/tests/test_totp.py": 9150389741890748343, + "backend/alembic.ini": 8897071018956157084, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/__init__.py": 10357835306532281617, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_groupby.py": 2738671407653333726, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_multiarray.py": 3592197154340095431, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tolist.py": 8343058000369672317, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_from_scalar.py": 16520136036631155842, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/lower_f2py_fortran.f90": 13315882216322269383, + "backend/src/schemas/profile.py": 6832850259859321131, + "docs/api/html/group__TestDictionaryCorrection.html": 17994177904345038747, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/logging.py": 2426447364230673649, + "venv/lib/python3.13/site-packages/numpy/_core/tests/_locales.py": 18178500267053825979, + "docs/api/html/group__handleSelectionChange__Function.html": 3955497001431142366, + "docs/api/html/globals_func_o.html": 8547213010974082815, + "docs/api/html/group__update__environment__schedule.html": 2300037495501071972, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_color.py": 3700828402084646308, + "docs/api/html/group__DeployRequest.html": 15085302046619241640, + "venv/lib/python3.13/site-packages/itsdangerous/signer.py": 3694661094753475840, + "venv/lib/python3.13/site-packages/ecdsa/test_numbertheory.py": 2117130219401053225, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py": 3203921571999101793, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/__init__.py": 15340047930888693984, + "docs/api/html/group__role__permissions.html": 12460381868773175294, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_dataframe.py": 14945138617962156057, + "frontend/src/lib/utils.js": 2362133402815996206, + "venv/lib/python3.13/site-packages/git/refs/tag.py": 9979537867501757110, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.pyi": 1830368009811266890, + "docs/api/html/group__ContextAwarePromptBuilder.html": 7714816079153271995, + "venv/lib/python3.13/site-packages/packaging/metadata.py": 9168255069163141470, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/__init__.py": 15130871412783076140, + "docs/api/html/group__update__job.html": 3867501244194585555, + "venv/lib/python3.13/site-packages/pygments/lexers/pony.py": 2720635393743617141, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.f": 18215492757907465667, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_almost_equal.py": 9333355453468475330, + "backend/git_repos/remote/test-repo.git/objects/5d/9eb555c4cac2d3ffdba84c07682b154fe4180d": 10756202165146702691, + "docs/api/html/group__run__compliance__violations.html": 7576622887921005784, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/decl_class.py": 6328411894571735935, + "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiPut.html": 539702435765100928, + "docs/api/html/group__test__get__logs__with__filters.html": 8248101620464596303, + "venv/lib/python3.13/site-packages/jeepney/io/common.py": 15160647397460834508, + "docs/api/html/group__start__maintenance.html": 4069649424458753421, + "venv/lib/python3.13/site-packages/rapidfuzz/utils.py": 5682627611354455144, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.cpython-313-x86_64-linux-gnu.so": 9014806524727977333, + "docs/api/html/group__test__us2__clarification__endpoints__persist__answer__and__feedback.html": 8985005200241520206, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.pyf": 1361426175133186231, + "venv/lib/python3.13/site-packages/uvicorn/loops/asyncio.py": 16496916527760969173, + "docs/api/html/group__test__tasks__logs__module.html": 17382599826426935479, + "docs/api/html/group__DatasetProfileDto.html": 15394265151763865885, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_take.py": 3281943380342853080, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/search/all_4.js": 1005255247300836207, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_setops.py": 17350958906336246626, + "docs/api/html/group__test__abort__controller.html": 12820594964734088755, + "docs/api/html/group__test__with__source.html": 12054958327519265597, + "docs/api/html/group__auto__size__batches.html": 1896970568485171466, + "docs/api/html/group____dataset__review__conflict__http__exception.html": 2685921445603174181, + "docs/api/html/group__startDataTimeout__Function.html": 93996475289484245, + "docs/api/html/group__api__include__routers.html": 15917122012230549418, + "venv/lib/python3.13/site-packages/jsonschema/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.pyi": 2186875333484796630, + "docs/api/html/group__TestGitPluginConfigIntegration.html": 5014758280841319650, + "venv/lib/python3.13/site-packages/sqlalchemy/event/api.py": 987321343916500337, + "venv/lib/python3.13/site-packages/PIL/FitsImagePlugin.py": 618556384932056120, + "docs/api/html/group__ReportCollection.html": 7174779192702599484, + "venv/lib/python3.13/site-packages/pydantic/v1/validators.py": 1060954771151908883, + "docs/api/html/globals_o.html": 15925482099999156324, + "docs/api/html/group__test__constants__are__immutable.html": 18426400293933334698, + "docs/api/html/group__DashboardHub__normalizeValidationStatus__Function.html": 6303647604098249484, + "venv/lib/python3.13/site-packages/websockets/speedups.pyi": 10322382420215791091, + "docs/api/html/group__test__get__remote__ids__batch__returns__dict.html": 9840452755032662260, + "docs/api/html/group__MigrationApi.html": 16067246055200738118, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_repeat.py": 11360772447747837958, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_header.py": 3564391095263545879, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/compiler.py": 11662494978292401568, + "venv/lib/python3.13/site-packages/pydantic/v1/mypy.py": 17076910404409672245, + "venv/lib/python3.13/site-packages/pygments/lexers/oberon.py": 15220143799603451830, + "venv/lib/python3.13/site-packages/pygments/lexers/__init__.py": 4627540245914849670, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_cumulative.py": 11752712818547022872, + "docs/api/html/group__Debounce__Module.html": 15506061807938542793, + "venv/lib/python3.13/site-packages/passlib/handlers/roundup.py": 3844280972451753019, + "docs/api/html/group__test__get__remote__id__returns__none__for__missing.html": 14781894002344872627, + "venv/lib/python3.13/site-packages/pillow.libs/libavif-01e67780.so.16.3.0": 13947441788826399574, + "docs/api/html/group__export__validation.html": 13641774792475599780, + "venv/lib/python3.13/site-packages/dateutil/_common.py": 12492546559232079071, + "venv/lib/python3.13/site-packages/git/exc.py": 14278238357119816365, + "venv/lib/python3.13/site-packages/pip/_internal/network/download.py": 5256731341219268693, + "docs/api/html/group__get__repository__status__batch.html": 5326587489507383540, + "docs/api/html/group____get__run__service.html": 2727986697505048178, + "docs/api/html/group__record__field__feedback.html": 1747834751721830995, + "frontend/src/routes/tools/storage/+page.svelte": 8817697925409987305, + "docs/api/html/group__get__resource__service.html": 18383236607747383835, + "docs/api/html/group__MockConfigManager.html": 2192424076609045256, + "docs/api/html/search/functions_14.js": 8813812505202203027, + "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.py": 151202984060101867, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_describe.py": 362374422228593281, + "venv/lib/python3.13/site-packages/numpy/core/getlimits.py": 14257687540182552408, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_parser.py": 3540203683732744499, + "docs/api/html/group__test__get__sources.html": 11705664249388270176, + "venv/lib/python3.13/site-packages/pip/_internal/utils/packaging.py": 15555781487631855604, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/claims.py": 12645788445743898136, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isna.py": 18394053826166267096, + "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/test_rapidfuzz_packaging.py": 5189055608193366093, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/cache_key.py": 11118819065204404106, + "docs/api/html/group__normalize__task__report.html": 17645268155877338532, + "venv/lib/python3.13/site-packages/pyasn1/codec/streaming.py": 13202773554072937140, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/_spdx.py": 17182188548503720124, + "venv/lib/python3.13/site-packages/pluggy/_warnings.py": 2664977718908223842, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py": 7107619871245389835, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__SupersetContextTemplatesMixin____normalize__default__literal.html": 8062184899607586465, + "docs/api/html/globals_w.html": 15465521333184498068, + "venv/lib/python3.13/site-packages/starlette/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_iloc.py": 16160558158155038350, + "frontend/build/_app/immutable/nodes/35.HskY9feQ.js": 9337575344793780337, + "docs/api/html/group__test__sql__table__extractor.html": 10962497480453520163, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jws_algs.py": 11920621906391796226, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/keywrap.py": 10424037418528425800, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____getSettings.html": 17142414359252665834, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.pyf": 10179237442840865225, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/METADATA": 2053484635654179443, + "docs/api/html/group__MaintenanceDashboardBannerState.html": 9967895785618239725, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_timegrouper.py": 276805119790266454, + "venv/lib/python3.13/site-packages/websockets/asyncio/compatibility.py": 9716993942839407921, + "docs/api/html/group____pii__imports.html": 1295891648143980104, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_promote.py": 16601872688404846307, + "frontend/playwright-report/trace/playwright-logo.svg": 4728037962520658573, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_take.py": 9847808489301498487, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_values.py": 430620320423334461, + "venv/lib/python3.13/site-packages/sqlalchemy/schema.py": 16029542421991709659, + "docs/api/html/group__frontend__src__lib__utils__js____cn.html": 8947971455954097415, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_monotonic.py": 11817523305776951162, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_take.py": 1216902953519121176, + "venv/lib/python3.13/site-packages/jeepney/io/asyncio.py": 13239992245695516007, + "backend/tests/test_task_persistence.py": 961578077605074697, + "venv/lib/python3.13/site-packages/pygments/lexers/graphics.py": 13783143864729682444, + "venv/lib/python3.13/site-packages/authlib/jose/util.py": 14843923624043422219, + "docs/api/html/group__MarkerLogger.html": 12923422177561303819, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_reflection.py": 10520778598547003532, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.py": 17112989332785349705, + "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.pyi": 7674087576653964854, + "docs/api/html/group__delete__dictionary.html": 8818744411907449226, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/index_tricks.pyi": 11463747060334597941, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_indexing.py": 81180636019259296, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_partial_indexing.py": 4132091154215910029, + "docs/api/html/group__resume__clarification.html": 15214230648786619854, + "venv/lib/python3.13/site-packages/attrs/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/METADATA": 13428590545328703138, + "venv/lib/python3.13/site-packages/pygments/lexers/_mql_builtins.py": 2376212048709413586, + "docs/api/html/group__NavigationContracts.html": 585565417315368787, + "docs/api/html/group____resolve__environment.html": 16547133577275735340, + "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.py": 11758491058846790938, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_can_hold_element.py": 5406431247116557833, + "frontend/src/lib/components/layout/Sidebar.svelte": 18409661059413656763, + "frontend/src/routes/dashboards/[id]/components/DashboardLinkedResources.svelte": 2349700459673295788, + "frontend/static/favicon.png": 16740551781088792605, + "venv/lib/python3.13/site-packages/pandas/io/clipboard/__init__.py": 3656451154829206939, + "backend/src/models/__tests__/test_report_models.py": 1182685421054960611, + "docs/api/html/group__test__get__dashboard__thumbnail__success.html": 9479616945914668390, + "venv/lib/python3.13/site-packages/pydantic/deprecated/json.py": 17692639396786690144, + "venv/lib/python3.13/site-packages/pandas/tests/construction/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/make.py": 5345282370426217663, + "venv/lib/python3.13/site-packages/ecdsa/_rwlock.py": 7971972922423146201, + "venv/lib/python3.13/site-packages/fastapi/dependencies/utils.py": 7687214523082385505, + "frontend/src/lib/components/reports/__tests__/report_card.ux.test.js": 10328656559485751380, + "docs/api/html/group__test__map__columns__empty__ids.html": 6731261812856257693, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_util.py": 16024147572807865219, + "venv/lib/python3.13/site-packages/PIL/PaletteFile.py": 4104323659569809769, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/utils.py": 617314300548081590, + "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.py": 17277594393032382485, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/METADATA": 8128818962678047497, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__addToast__Function.html": 13046327232086323563, + "venv/lib/python3.13/site-packages/referencing/__init__.py": 10093824711214801917, + "venv/lib/python3.13/site-packages/pandas/tests/test_flags.py": 411845322592830527, + "venv/lib/python3.13/site-packages/passlib/tests/test_apps.py": 10454136535890937237, + "docs/api/html/group__selectPlugin__Function.html": 7924969411262828408, + "backend/src/plugins/translate/service_datasource.py": 10240814407336170041, + "venv/lib/python3.13/site-packages/pygments/lexers/mime.py": 7768464764849718825, + "docs/api/html/group__test__migrate__dashboards__success.html": 13646594525073241233, + "venv/lib/python3.13/site-packages/_pytest/tmpdir.py": 10834220332037364219, + "venv/lib/python3.13/site-packages/pip/_internal/commands/show.py": 11839987586497822300, + "docs/api/html/group__TestCleanReleaseSourcePolicy.html": 16395844908630771518, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/greenlet_exceptions.hpp": 8819301638282185623, + "docs/api/html/group__test__delete__run__not__found.html": 14187887394068528264, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_array.f90": 9711940829820746513, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_algs.py": 2479791018067637690, + "docs/api/html/group__getPermissions__Function.html": 4129456418206064840, + "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector_cpp.cpython-313-x86_64-linux-gnu.so": 16337157355618216738, + "docs/api/html/group____check__any__permission.html": 4366102498601457818, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/highlighter.py": 10615901258492570740, + "venv/lib/python3.13/site-packages/_pytest/mark/__init__.py": 9343015549300275714, + "venv/lib/python3.13/site-packages/h11/_events.py": 13800479928851716538, + "docs/api/html/group__delete__file.html": 5881932655140602026, + "docs/api/html/globals_v.html": 10195887458242636103, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/INSTALLER": 17282701611721059870, + "backend/src/api/routes/dashboards/_action_routes.py": 10305887713397749487, + "docs/api/html/group__DashboardDetailResponse.html": 10795760041340688929, + "docs/api/html/group__AssistantApiTests.html": 15253113969203355593, + "docs/api/html/group__get__preview__records.html": 2916851160069655824, + "venv/lib/python3.13/site-packages/greenlet/tests/test_stack_saved.py": 7767924647054186572, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_common.f": 5913807803369559749, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nlargest_nsmallest.py": 11049656379959696695, + "venv/lib/python3.13/site-packages/pandas/io/excel/_openpyxl.py": 6676451564208867775, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_py_collections.py": 16825222625411635946, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/sql.py": 13315906751371033170, + "docs/api/html/globals_func_a.html": 13712688806462460438, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/vip.py": 11287963054551122004, + "venv/lib/python3.13/site-packages/pygments/lexers/hare.py": 8693002231020751711, + "docs/api/html/group__retry__run.html": 9488774948219266966, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/base.py": 14734745827490220667, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/WHEEL": 2357997949040430835, + "docs/api/html/group__build__distribution__manifest.html": 9519354018984408473, + "venv/lib/python3.13/site-packages/certifi/cacert.pem": 14727832973577663826, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_datetimeindex.py": 14816894350435545024, + "docs/api/html/group__CommitCreate.html": 3838920800459063824, + "venv/lib/python3.13/site-packages/httpcore/_trace.py": 9576253018891841832, + "docs/api/html/group__SidebarStoreTest__Module.html": 10062386876351645240, + "docs/api/html/group__test__lookup__superset__accounts__success.html": 21505406146454782, + "venv/lib/python3.13/site-packages/pygments/styles/fruity.py": 7925019839136894457, + "docs/api/html/group__handleBulkCommit__Function.html": 13824296665546126425, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_packbits.py": 7592956764084658562, + "venv/lib/python3.13/site-packages/keyring/errors.py": 4587094338672775040, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_period.py": 15671388508498441412, + "frontend/src/routes/settings/EnvironmentsTab.svelte": 13575164255656888464, + "docs/api/html/group__approve__batch__semantic__fields.html": 3745065335953800486, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.py": 6617116180936986481, + "docs/api/html/group__SessionCollectionResponse.html": 6802234872096434048, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.pyi": 11967691665766452312, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_where.py": 16584772339591698754, + "backend/src/schemas/auth.py": 5405968347063927002, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_missing.py": 2976310411820303374, + "docs/api/html/group__LaunchDatasetCommand.html": 9231022036284145569, + "docs/api/html/group__add__translation__job.html": 715901058035169272, + "docs/api/html/group__import__artifacts.html": 3620849422629917960, + "venv/lib/python3.13/site-packages/pytest_asyncio/plugin.py": 13414541723622354359, + "docs/api/html/group__get__filename__from__headers.html": 457671362301086919, + "docs/api/html/group__GitRepoLifecycleRoutes.html": 8015073860128542249, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.cpython-313-x86_64-linux-gnu.so": 16604252871044637047, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ufuncobject.h": 5112456258078662871, + "docs/api/html/group__MaintenanceBannerRenderer.html": 6099946816052023316, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE": 14610443802372107702, + "venv/lib/python3.13/site-packages/dotenv/variables.py": 5438726379749803751, + "venv/lib/python3.13/site-packages/annotated_doc/main.py": 3384049281532298500, + "docs/api/html/group__AssistantDatasetReviewDispatch.html": 15266638409291242280, + "docs/api/html/group__test__auth.html": 2421668339643460366, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____endAllMaintenance.html": 9764396289727492852, + "venv/lib/python3.13/site-packages/rsa/util.py": 13423880475585138638, + "docs/api/html/group__GitReleasePanel.html": 2228780701983179318, + "frontend/e2e/tests/live-project-check.e2e.js": 4657915737640702974, + "backend/tests/test_storage_config.py": 14773544217703363468, + "venv/lib/python3.13/site-packages/jsonschema/_keywords.py": 949562792260528762, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_matmul.py": 16169282225324014901, + "docs/api/html/group__MaintenanceRouter.html": 16070342961663373297, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply_relabeling.py": 5600949862632014700, + "frontend/src/lib/components/translate/TranslationPreview.svelte": 9514416214545061268, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_codes.py": 13901854719435716195, + "venv/lib/python3.13/site-packages/typing_inspection/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/greenlet_allocator.hpp": 11041111583319491309, + "docs/api/html/group__test__get__dashboards__override__show__all__contract.html": 16206749140891179616, + "docs/api/html/group__HealthSchemas.html": 5884366198523374362, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_pickle.py": 3419420815386925630, + "venv/lib/python3.13/site-packages/websockets/asyncio/async_timeout.py": 7267209982032642028, + "venv/lib/python3.13/site-packages/smmap/test/lib.py": 2924931247676024085, + "venv/lib/python3.13/site-packages/numpy/lib/mixins.pyi": 15597536290223628777, + "docs/api/html/group__setAssistantConversationId__Function.html": 7465015029614231837, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_lib.py": 6721066015604956469, + "docs/api/html/group____make__manager.html": 16413916660006283470, + "venv/lib/python3.13/site-packages/pandas/core/interchange/column.py": 9498044216416177656, + "docs/api/html/group__load__tasks.html": 13194384238466330387, + "venv/lib/python3.13/site-packages/jose/backends/ecdsa_backend.py": 2684176977865283060, + "docs/api/html/group__extract__tables__from__sql.html": 8074608921519700652, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_csv.py": 10737444103906389448, + "frontend/src/lib/Counter.svelte": 3616557554460116236, + "docs/api/html/group__NotificationService.html": 14374014839364668482, + "backend/src/models/dataset_review_pkg/_mapping_models.py": 18201159148567381838, + "venv/lib/python3.13/site-packages/pydantic/tools.py": 8931184638883701008, + "venv/lib/python3.13/site-packages/referencing/_core.py": 14744795336121513230, + "venv/lib/python3.13/site-packages/numpy/lib/tests/__init__.py": 15130871412783076140, + "docs/api/html/group__log__security__event.html": 18315452659527006860, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_results.py": 18145930285449734975, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/aiomysql.py": 12999962380333482189, + "docs/api/html/group__format__timezone__offset.html": 14635780330486688343, + "venv/lib/python3.13/site-packages/pygments/lexers/fift.py": 7537570342576781911, + "docs/api/html/group____handle__network__error.html": 15557655064289466036, + "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.pyi": 6286629088867436848, + "venv/lib/python3.13/site-packages/dotenv/main.py": 7408889670575271484, + "docs/api/html/group____require__auto__review__flag.html": 8794268766415982314, + "backend/src/api/routes/assistant/_tool_commit.py": 11137100312515201186, + "docs/api/html/group__ProfileAuthorizationError.html": 17298627696052003006, + "docs/api/html/group____make__httpx__status__error.html": 1809671805993904222, + "venv/lib/python3.13/site-packages/idna/uts46data.py": 5698404064763638591, + "docs/api/html/group__test__get__profile__preferences__returns__self__payload.html": 17928297903681882891, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py": 13215932786801020434, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.pyi": 6546751036506752550, + "docs/api/html/group__handleUpload__Function.html": 10159956200065592690, + "frontend/src/lib/components/health/ScheduleAtAGlance.svelte": 2047418178814771337, + "frontend/src/components/llm/ProviderConfig.svelte": 18108375341352259203, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/RECORD": 7320381591791379627, + "venv/lib/python3.13/site-packages/uvicorn/middleware/asgi2.py": 5355343704425405009, + "venv/lib/python3.13/site-packages/pygments/lexers/eiffel.py": 8964563077187051946, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py": 2492962412506082234, + "backend/src/api/routes/plugins.py": 6640856303389338188, + "docs/api/html/group__get__log__stats.html": 6749080858232448787, + "venv/lib/python3.13/site-packages/pandas/api/extensions/__init__.py": 882327406374020760, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bindgen.py": 8637878018232418583, + "docs/api/html/group__AsyncAPIClient______init____.html": 2185805261011326243, + "docs/api/html/group__test__create__isolated__repository__keeps__mode__data__separate.html": 8351843508860649482, + "docs/api/html/group__ExecutionMapping.html": 9160979664327901038, + "docs/api/html/search/all_f.js": 12596024952098859181, + "docs/api/html/group__test__tui__clear__history__f7.html": 2956037542999638884, + "venv/lib/python3.13/site-packages/starlette/types.py": 6372850128669191435, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/__init__.py": 11009955319062089477, + "docs/api/html/group__MigrationMappingsTable.html": 14769683728212158945, + "venv/lib/python3.13/site-packages/ecdsa/test_der.py": 7117444285097824495, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/pretty.py": 2448541115957861214, + "venv/lib/python3.13/site-packages/pandas/_typing.py": 17795648994662434211, + "venv/lib/python3.13/site-packages/PIL/ImageMath.py": 7395449734007598797, + "docs/api/html/group__frontend__src__lib__stores__sidebar__js____toggleSidebar.html": 1585062636957734877, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/common.py": 10996276696597806957, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_formats.py": 13752366280743004410, + "docs/api/html/group__FileIO.html": 16081394467127736340, + "docs/api/html/group__TestGitStatusRoute.html": 2805903254981550979, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_formats.py": 10263085276270778330, + "frontend/build/_app/immutable/chunks/CQOzILqt.js": 13919938634888964115, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_coercion.py": 3551994404797560483, + "venv/lib/python3.13/site-packages/tzlocal/windows_tz.py": 11696765495002954868, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/device_code.py": 6712610002227849281, + "docs/api/html/group__update__metric__description.html": 15940615433146474716, + "venv/lib/python3.13/site-packages/git/objects/submodule/root.py": 5322248117564025370, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/utils.py": 2959395751217827103, + "backend/src/models/git.py": 14752775624662976821, + "docs/api/html/group____seed__legacy__repo.html": 8385616245583706934, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npz": 9481735723557962988, + "docs/api/html/group__test__expired__key__raises__401.html": 14311462003347056857, + "docs/api/html/group__get__history.html": 18146329279871398582, + "venv/lib/python3.13/site-packages/pluggy/_callers.py": 1042673447718196223, + "frontend/src/routes/tools/mapper/+page.svelte": 9825534971683180074, + "docs/api/html/group__test__dashboard__health__item__status__accepts__only__whitelisted__values.html": 136153182983638913, + "venv/lib/python3.13/site-packages/pandas/io/formats/excel.py": 797619981868084011, + "docs/api/html/group__RemoteRepoSchema.html": 11973800120981189119, + "docs/api/html/group__DictionaryImportExport.html": 4551906910630157436, + "docs/api/html/group__test__update__task__not__found.html": 14144153955621354369, + "docs/api/html/group__test__single__line__text.html": 2397299118737677285, + "docs/api/html/group__runBulkGitAction__Function.html": 11579106801115881689, + "venv/lib/python3.13/site-packages/pydantic/_internal/_serializers.py": 12077180322353709930, + "docs/api/html/group____build__target__signatures.html": 16982125749763522469, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/base.py": 13269563625127294513, + "docs/api/html/group__frontend__src__services__storageService__js____encodeStoragePath.html": 13531232492438542038, + "venv/lib/python3.13/site-packages/uvicorn/protocols/utils.py": 6147480543268737542, + "docs/api/html/group__test__list__runs__invalid__status.html": 987375723925520985, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/instrumentation.py": 14882045449667214075, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/asn1.pyi": 16048852379621461316, + "docs/api/html/group__Task.html": 4118191587870262013, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.py": 4672871351134074735, + "docs/api/html/group__MaintenanceSettingsUpdate.html": 5565369427863367370, + "docs/api/html/group__test__transform__zip__end__to__end.html": 11478692460747852800, + "venv/lib/python3.13/site-packages/apscheduler/executors/twisted.py": 2695602672085615762, + "venv/lib/python3.13/site-packages/numpy/core/numeric.py": 17025007229779320230, + "backend/src/core/encryption.py": 6401049364294007361, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____init.html": 6160087973904348720, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated": 11193150813314469394, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_re.py": 1296579393600719224, + "venv/lib/python3.13/site-packages/pip/_internal/commands/lock.py": 14782882714168174465, + "docs/api/html/group__test__backup__dashboards__success.html": 15648044281266776906, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_scrypt.py": 17378030264314664250, + "venv/lib/python3.13/site-packages/jsonschema_specifications/__init__.py": 5732822354953774657, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_value_counts.py": 12340872812120847127, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_formats.py": 1265319125235346745, + "backend/src/core/utils/matching.py": 1700014874878218207, + "venv/lib/python3.13/site-packages/passlib/tests/utils.py": 15403198680231294263, + "docs/api/html/group__TaskDrawerTests.html": 15575873839723255766, + "docs/api/html/group__normalizeApiError__Function.html": 11798259322678734628, + "venv/lib/python3.13/site-packages/pandas/core/interchange/utils.py": 11401792151107207834, + "docs/api/html/group__ApiKeysTab.html": 8274883643642903708, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/RECORD": 173225030102092319, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/poly1305.py": 10854598965202354516, + "docs/api/html/group__test__setActiveItem__Function.html": 8801230193725679964, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/WHEEL": 16784970174376303810, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/METADATA": 10241403930811743893, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_style.py": 10405592363651914796, + "venv/lib/python3.13/site-packages/pip/_internal/utils/entrypoints.py": 11268059714209889385, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_parse_dates.py": 7597935994945336408, + "backend/src/api/routes/__tests__/conftest.py": 10021153829095639891, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_cov_corr.py": 8117511855230249212, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/assertion.py": 5287436128235488464, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/einsumfunc.pyi": 8902765738896588686, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_reductions.py": 8258332365408303156, + "docs/api/html/group__test__extract__native__filters__from__permalink__direct__response.html": 2976726265511560969, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/value_attrspec/gh21665.f90": 11945588837236968550, + "venv/lib/python3.13/site-packages/pygments/lexers/asm.py": 13861952972248758785, + "backend/src/core/task_manager/models.py": 18323833834608444445, + "venv/lib/python3.13/site-packages/dateutil/_version.py": 8279235793001245767, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_as_unit.py": 4283520188709399244, + "docs/api/html/group__RepositoryBindingSchema.html": 7912861215581324084, + "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.py": 12796890117561096360, + "docs/api/html/group__MigrationMappingsPage.html": 17363977661933102131, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_missing.py": 3991199131191840903, + "docs/api/html/nav_fd.png": 7284442651853922987, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/__init__.py": 15130871412783076140, + "backend/src/plugins/translate/orchestrator_cancel.py": 673611364664367049, + "docs/api/html/group__SupersetClientGetCharts.html": 6360695148314682778, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/__init__.py": 9482393721453335937, + "docs/api/html/group__test__get__git__configs__masks__pat.html": 6988882008771183656, + "venv/lib/python3.13/site-packages/anyio/from_thread.py": 7973678341601453474, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/segment.py": 2555584120510872966, + "venv/lib/python3.13/site-packages/pip/_internal/exceptions.py": 2252826732781519645, + "docs/api/html/group__docker__backend__entrypoint__bootstrap__admin.html": 4653794695033998401, + "docs/api/html/group__unsubscribe__logs.html": 14174454111096175560, + "docs/api/html/group__SupersetClientValidateQueryParams.html": 1288505283032735283, + "docs/api/html/group__test__list__runs__pagination.html": 11299418008517910530, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_hashtable.py": 2620207240930665390, + "venv/lib/python3.13/site-packages/pandas/core/computation/expr.py": 13085709365673019494, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype.py": 7796703383405149774, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd_module.py": 7122050193390298178, + "venv/lib/python3.13/site-packages/sqlalchemy/log.py": 10101221585967121334, + "docs/api/html/group__TaskLoggerModule.html": 11856387495799127523, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/exc.py": 1448374203226983757, + "docs/api/html/group__TranslationUtils.html": 10386648824429307238, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_delete.py": 5958440386632506587, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_normalize.py": 11373923576259976755, + "frontend/src/routes/validation/+page.svelte": 9845194549295283559, + "backend/src/plugins/translate/__tests__/test_dictionary_import.py": 17094877730934121606, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi": 1626154870044880426, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_deprecations.py": 8310244516175542839, + "backend/test_auth.db": 18214842050920965999, + "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.pyi": 18193727720492050602, + "venv/lib/python3.13/site-packages/more_itertools/more.py": 1631135582441184133, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_legendre.py": 18266595775500927868, + "venv/lib/python3.13/site-packages/itsdangerous/_json.py": 4872267972688724005, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/annotation.py": 15855576199737688124, + "backend/src/plugins/translate/__tests__/test_target_schema.py": 12975409291774047788, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/__init__.py": 14268033439436797364, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_api.py": 10514120091521589282, + "venv/lib/python3.13/site-packages/jsonschema_specifications/_core.py": 14812112544772987164, + "venv/lib/python3.13/site-packages/python_multipart/exceptions.py": 5971058243218049183, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/uts46data.py": 1867240043777383728, + "venv/lib/python3.13/site-packages/_pytest/_code/code.py": 5248836309618416369, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py": 17538029557373507617, + "docs/api/html/group__triggerRun__Function.html": 981175398640370870, + "venv/lib/python3.13/site-packages/pygments/lexers/slash.py": 8320676560518108343, + "backend/src/services/reports/__tests__/test_report_service.py": 2262310201779987221, + "docs/api/html/group__handleSaveRole__Function.html": 5310344647439892257, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_concat.py": 10207975532842927134, + "venv/lib/python3.13/site-packages/pygments/lexers/markup.py": 10311057264960923433, + "docs/api/html/group__TestPolicyResolutionService.html": 11614920422741434476, + "docs/api/html/group__docker__backend__Dockerfile.html": 9397362251378611899, + "venv/lib/python3.13/site-packages/pandas/tests/io/__init__.py": 15130871412783076140, + "docs/api/html/group__estimate__token__budget__for__rows.html": 9229322075799591152, + "docs/api/html/group__verifySessionAndAccess__Function.html": 6518276629640187972, + "docs/api/html/group__test__get__session__detail__export__and__lifecycle__endpoints.html": 7302188143370256832, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/licenses/LICENSE": 7959949171752474553, + "docs/api/html/group__MaintenanceStoreTests.html": 14540516874541556957, + "venv/lib/python3.13/site-packages/pyasn1/codec/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunclike.py": 12077553320554633586, + "docs/api/html/group__trigger__sync__now.html": 3396264963221246266, + "docs/api/html/group__handleGenerateMessage__Function.html": 11846603189188412357, + "venv/lib/python3.13/site-packages/httpx/_content.py": 1301854837767395687, + "backend/src/plugins/translate/__init__.py": 12400577052797809611, + "venv/lib/python3.13/site-packages/pandas/core/computation/__init__.py": 15130871412783076140, + "docs/api/html/group__bundle__build__mode.html": 16344595277983678406, + "docs/api/html/group__DashboardHub__formatDate__Function.html": 6545488449073827576, + "docs/api/html/group__DatasetMapper.html": 8174296786664861741, + "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimes.py": 10585034798097337148, + "docs/api/html/group__GitServerConfigUpdate.html": 4756737950210180794, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/WHEEL": 2357997949040430835, + "docs/api/html/search/pages_1.js": 3711959322811078222, + "venv/lib/python3.13/site-packages/passlib/handlers/phpass.py": 16489430001449160264, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_compare.py": 171535659467902751, + "backend/src/services/notifications/providers.py": 12802217818475476969, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/algorithms.py": 17106903014205241774, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_repr.py": 13147587049887075335, + "docs/api/html/group____make__us2__session.html": 6547387069315862625, + "backend/src/api/routes/git/_repo_lifecycle_routes.py": 410621371876058173, + "docs/api/html/group__resume__task.html": 10211397007728257393, + "docs/api/html/group__test__release__candidate__valid.html": 9747216871494350774, + "backend/git_repos/remote/test-repo.git/hooks/fsmonitor-watchman.sample": 14381968406977928721, + "frontend/tests/maintenance.test.ts": 18128393571681331195, + "venv/lib/python3.13/site-packages/pygments/lexers/other.py": 6893672181003034910, + "docs/api/html/group__handleDownload__Function.html": 4146074044892202143, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/METADATA": 15876173086384975957, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/METADATA": 3474626408138844944, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test__exceptions.py": 12099001631178696189, + "venv/lib/python3.13/site-packages/click/py.typed": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/refs/heads/master": 12485529318667335076, + "venv/lib/python3.13/site-packages/httpcore/_async/http_proxy.py": 13364803556177997526, + "venv/lib/python3.13/site-packages/httpx/_types.py": 12461152436147308761, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_copy.py": 3114918520437707410, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_ssl_constants.py": 14834514243239777933, + "backend/src/plugins/translate/__tests__/test_dictionary.py": 7810983986142971202, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____startTranslationRun.html": 11102301560013944310, + "docs/api/html/group__StartCheckRequest.html": 3154093382504996764, + "docs/api/html/group__transform__zip.html": 10792708138270698712, + "venv/lib/python3.13/site-packages/pydantic/plugin/_schema_validator.py": 857640644840976862, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/linalg.pyi": 269481015135399748, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_seed_sequence.py": 7687465232954593311, + "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.cpython-313-x86_64-linux-gnu.so": 9380059465932560412, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_where.py": 14167991754625133668, + "docs/api/html/group__UserCreate.html": 5250366849739606333, + "docs/api/html/group__frontend__src__services__git__utils__js____normalizeEnvStage.html": 13961356311527011805, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_orc.py": 1248059610437937895, + "frontend/src/lib/components/translate/RunTabContent.svelte": 13755345255986436394, + "venv/lib/python3.13/site-packages/numpy/random/_common.cpython-313-x86_64-linux-gnu.so": 12834724130890991478, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_halfyear.py": 9376272342364384767, + "docs/api/html/group__ClarificationOption.html": 4855555471647664817, + "docs/api/html/group__ApiHelper.html": 11728817047769872638, + "venv/lib/python3.13/site-packages/pandas/io/orc.py": 12754290195886557668, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/RECORD": 10052481303799123207, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____getProviderById.html": 6376806405547386425, + "docs/api/html/group__RemoteRepoCreateRequest.html": 12603451785316041896, + "frontend/build/_app/immutable/nodes/2.D9rtGwdH.js": 10721133077652006043, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/accumulate.py": 5780225603819849877, + "docs/api/html/group__test__all__keys__existing__returns__empty.html": 2310303904698481345, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/METADATA": 6502714564886871819, + "frontend/src/components/storage/FileList.svelte": 6226735853135032532, + "docs/api/html/group__id.html": 4811327565109647473, + "docs/api/html/group__get__dictionary.html": 6783587429209921357, + "venv/lib/python3.13/site-packages/numpy/core/function_base.py": 12696640832638144604, + "venv/lib/python3.13/site-packages/pygments/lexers/jsonnet.py": 1314044008440065525, + "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.pyi": 15130871412783076140, + "docs/api/html/group__warning.html": 8389706144157265523, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/fromnumeric.pyi": 6708743411405679061, + "docs/api/html/group__SupersetContextRecoveryMixin____normalize__imported__filter__payload.html": 13846106026428255506, + "venv/bin/pyrsa-priv2pub": 16766314338551408621, + "backend/src/services/dataset_review/clarification_pkg/_helpers.py": 10217926576108486717, + "docs/api/html/group__test__parse__superset__link__dashboard__partial__recovery.html": 3627304491605755157, + "venv/lib/python3.13/site-packages/pandas/core/window/rolling.py": 6316477981414191778, + "backend/src/services/clean_release/repositories/audit_repository.py": 1431160646630812395, + "docs/api/html/group__TranslationRunLanguageStatsResponse.html": 13012330439222660734, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/crud.py": 805684028038896506, + "docs/api/html/group____parse__args.html": 15401654965875624913, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.cpython-313-x86_64-linux-gnu.so": 7840846965295537844, + "docs/api/html/group__get__config__manager.html": 9948904039819241913, + "docs/api/html/group__test__api__delete__job__not__found.html": 9283924924862498850, + "docs/api/html/group__DatasetReviewSessionModels.html": 15486238525446211823, + "docs/api/html/group__i18n__Module.html": 10632521022228083958, + "venv/lib/python3.13/site-packages/annotated_types/py.typed": 15130871412783076140, + "backend/src/plugins/translate/_llm_parse.py": 3240723108319948544, + "docs/api/html/group__SessionPhase.html": 10683758452248873915, + "venv/lib/python3.13/site-packages/fastapi/security/base.py": 15674297568104917749, + "docs/api/html/group____serialize__clarification__question__payload.html": 2145294211221341001, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_timedeltaindex.py": 2359681317403710563, + "venv/lib/python3.13/site-packages/pandas/core/indexes/period.py": 1181348384346239378, + "venv/lib/python3.13/site-packages/pip/_internal/network/session.py": 293771075394156394, + "frontend/build/_app/immutable/nodes/20.0fu2pVCX.js": 3927402113240528958, + "backend/git_repos/remote/test-repo.git/objects/22/c0ea49cea7e5138fb91b25b1bc0e101ae6e017": 17630871400599334193, + "venv/lib/python3.13/site-packages/pandas/_libs/_cyutility.cpython-313-x86_64-linux-gnu.so": 3372660143512868174, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_float.py": 10455718110956856820, + "venv/lib/python3.13/site-packages/pandas/io/excel/_odfreader.py": 2760916559416783928, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_datetime.py": 3499461736474530062, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/uvicorn/main.py": 5843333502356171120, + "backend/src/api/routes/git/_repo_routes.py": 12158952761025397291, + "backend/git_repos/remote/test-repo.git/objects/e9/1109abe29d5228675fecd6fc38a097147c090d": 15716642455993350072, + "docs/api/html/group__SupersetContextTemplatesMixin____extract__primary__jinja__identifier.html": 13395638216145831396, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_algos.py": 5356470832688354593, + "docs/api/html/group__SupersetAccountLookupRequest.html": 11189418222429477505, + "docs/api/html/group__loadConfigs__Function.html": 6456332498353022877, + "venv/lib/python3.13/site-packages/authlib/common/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/chararray.pyi": 17340224970247832290, + "docs/api/html/group__DashboardScope.html": 315673707971698397, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_insert.py": 15536994417338770457, + "docs/api/html/group__Icon.html": 13262643427544485857, + "docs/api/html/group__GitService.html": 5644147165416014917, + "venv/lib/python3.13/site-packages/numpy/_core/multiarray.py": 4484782354376574355, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_set.py": 14375107704596905983, + "venv/lib/python3.13/site-packages/cffi/vengine_gen.py": 6582900613549620556, + "venv/lib/python3.13/site-packages/ecdsa/curves.py": 812969717084691115, + "docs/api/html/group__test__calculate__schedule__very__small__window.html": 11155677236354096772, + "venv/lib/python3.13/site-packages/git/objects/__init__.py": 2110615482320355676, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_logical.py": 6818185860296232476, + "venv/lib/python3.13/site-packages/pygments/styles/paraiso_dark.py": 3976779833379189837, + "docs/api/html/group__confirm__operation.html": 3794428125492004515, + "docs/api/html/group__TestDictionaryImport.html": 7122493165124405852, + "docs/api/html/group__MarkerLogger__reason.html": 11360973389497670827, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.cpython-313-x86_64-linux-gnu.so": 6011605826065363427, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_constructors.py": 17219764538851313252, + "backend/src/services/clean_release/stages/manifest_consistency.py": 5890428472425990952, + "docs/api/html/group__CostEstimate.html": 1003350075352370602, + "backend/src/models/dataset_review_pkg/_execution_models.py": 16180790193269688646, + "venv/lib/python3.13/site-packages/pygments/lexers/theorem.py": 15666523910367201285, + "docs/api/html/group__count__remaining__questions.html": 777004046773548975, + "venv/lib/python3.13/site-packages/pyasn1/error.py": 11036037300994516747, + "venv/lib/python3.13/site-packages/pip/_internal/models/direct_url.py": 4619204214785230341, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_range.py": 9900908262700749861, + "docs/api/html/group____get__dictionary__entry__counts.html": 15029433129373486518, + "docs/api/html/group__test__delete__job.html": 2874894259644027669, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/licenses/LICENSE": 1641332741515411734, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_writers.py": 103545128256076790, + "docs/api/html/group__list__tasks.html": 2630490387896738201, + "docs/api/html/group__openAssistantChatWithContext__Function.html": 6437289261413282510, + "docs/api/html/group__DEFAULT__LLM__PROVIDER__BINDINGS.html": 1197817568915985055, + "venv/lib/python3.13/site-packages/urllib3/util/request.py": 9432067988603015849, + "frontend/src/components/tools/DebugTool.svelte": 387230895185467713, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.py": 183817263146423033, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/dtype.pyi": 12178008080078679769, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/conftest.py": 6137462119729731945, + "venv/lib/python3.13/site-packages/websockets/http11.py": 11022534176398053082, + "docs/api/html/group____json__safe__value.html": 1390575450608766153, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_callback.py": 1689177048527329209, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo90.f90": 12508663906609241323, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_sort_values.py": 15072305514178699398, + "venv/lib/python3.13/site-packages/pygments/lexers/basic.py": 12723021007042054402, + "ss-tools.bundle": 7535288224854827255, + "backend/git_repos/remote/test-repo.git/objects/aa/ea042a37c6f7bd010d91db93951328da7f01cc": 11936716919619613939, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____setTaskDrawerAutoOpenPreference.html": 8925927322956055906, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/authorization_server.py": 2491654903543600678, + "frontend/build/_app/immutable/nodes/6.DSAuUgVl.js": 14169560997005078427, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____createMaintenanceStore.html": 15666277817262057243, + "docs/api/html/group__MaintenanceFailedDashboard.html": 17647303195778429767, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_conversion.py": 9159276697339089788, + "frontend/build/_app/immutable/chunks/Cm_5KXwn.js": 5655007561775976303, + "docs/api/html/group__fixtures.html": 7724348357498077937, + "venv/lib/python3.13/site-packages/numpy/testing/__init__.py": 2831275254313750978, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__DatasetReviewRoutes.html": 6463487666472915050, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi": 7385072545198817771, + "frontend/src/routes/login/+page.svelte": 2655837067927461296, + "docs/api/html/sync_on.png": 14172530649108700754, + "docs/api/html/group__normalizePermissionRequirement__Function.html": 3697197038016374909, + "docs/api/html/group__rebuild__banner.html": 11712513970127576620, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_types.py": 7175518113263603341, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_astype.py": 563689238486630866, + "docs/api/html/group__test__list__empty.html": 7117128937256101280, + "docs/api/html/group__MetricsTable.html": 17283314771986202846, + "docs/api/html/group__DashboardURLFilterExtraction.html": 4077034173336465746, + "venv/lib/python3.13/site-packages/PIL/GribStubImagePlugin.py": 14954946414134984896, + "docs/api/html/group__NetworkError.html": 8895892229568746717, + "docs/api/html/group__test__get__reports__invalid__filter__returns__400.html": 7652617513834271963, + "frontend/build/_app/immutable/nodes/33.CuvbPuAu.js": 1509129106645171823, + "docs/api/html/group____commit__owned__session__mutation.html": 16349315849175268536, + "docs/api/html/group__AuthRepositoryModule.html": 8460217615305414048, + "docs/api/html/group__AuthLoggerModule.html": 4166355749229873269, + "docs/api/html/group__API__Routes.html": 3741949103355825147, + "docs/api/html/group__test__semantic__source__version__propagation__preserves__locked__fields.html": 12577203451483874098, + "venv/lib/python3.13/site-packages/pandas/_config/dates.py": 3863381688916127265, + "venv/lib/python3.13/site-packages/starlette/middleware/cors.py": 11384893784314197864, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_head_tail.py": 13047338070883375426, + "venv/lib/python3.13/site-packages/pygments/plugin.py": 18006138132596504101, + "frontend/e2e/fixtures/auth.fixture.js": 13628564118439575126, + "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.py": 9654828595008925976, + "venv/lib/python3.13/site-packages/sqlalchemy/future/__init__.py": 17688444679736059779, + "docs/api/html/group__AuthApi.html": 1701222343226214552, + "venv/lib/python3.13/site-packages/httpx/_api.py": 6697370469329778523, + "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.py": 7029935879540617057, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_quantile.py": 17110581927381599978, + "docs/api/html/group__SchedulerService.html": 9915139758062001823, + "frontend/build/_app/immutable/chunks/BBpvYbNb.js": 3437361990445138122, + "backend/src/api/routes/assistant/_routes.py": 3122441936440307231, + "docs/api/html/group__UserBase.html": 6482510430149113904, + "venv/lib/python3.13/site-packages/secretstorage/dhcrypto.py": 8860116996905452758, + "venv/lib/python3.13/site-packages/greenlet/greenlet_slp_switch.hpp": 1694714764745718414, + "docs/api/html/group__ServiceUtils.html": 9023919090808833531, + "docs/api/html/group__TestDictionaryLegacyHub.html": 9545067935919622524, + "docs/api/html/group__TranslateJobList.html": 13432818086805315799, + "venv/lib/python3.13/site-packages/_pytest/_code/__init__.py": 6211607282116942091, + "docs/api/html/group__BranchSchema.html": 986826748050343530, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_memmap.py": 8709963808115492688, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/__init__.py": 15130871412783076140, + "docs/api/html/group__DashboardHub__applyGridTransforms__Function.html": 6805737041086802084, + "docs/api/html/group__publication__repository.html": 13260610346382951650, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/array.py": 13040461386712513615, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/base.py": 7855217087916771270, + "venv/lib/python3.13/site-packages/pyasn1/type/char.py": 16518487114777185541, + "docs/api/html/group__TestNormalizeApiError__Class.html": 2259840618123379616, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_api_info.py": 16491629653122827368, + "backend/src/plugins/translate/_run_service.py": 16811199996533842842, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/zip-safe": 15240312484046875203, + "docs/api/html/group__frontend__src__lib__api__validation__js____updateTask.html": 5295403112831889335, + "docs/api/html/group__remove__translation__job.html": 17823493926961254166, + "backend/src/api/routes/datasets.py": 17115508339292076862, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/licenses/COPYING": 15270149301471737292, + "venv/lib/python3.13/site-packages/pydantic/experimental/__init__.py": 492399139963056461, + "docs/api/html/group__TargetSchemaHint.html": 10951669208141820307, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.py": 1948707545068786, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_na_values.py": 13527837495439739279, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_numba.py": 1668304729617150075, + "docs/api/html/group__MigrationDryRunOrchestratorModule.html": 996754244754452705, + "docs/api/html/group__LlmModels.html": 338390014476636, + "backend/git_repos/remote/test-repo.git/info/exclude": 6403833611826357791, + "frontend/build/_app/immutable/nodes/21.B4V7IsHk.js": 6241928990018432103, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/region.py": 12579403230062395563, + "docs/api/html/group__orchestrator__query.html": 186936932358706266, + "venv/lib/python3.13/site-packages/authlib/deprecate.py": 3563885283090710443, + "docs/api/html/group__reason.html": 7755071372929183522, + "venv/lib/python3.13/site-packages/pyasn1/type/opentype.py": 340146737126677655, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd.py": 7577408467041941595, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_reductions.py": 11624188672428941494, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiprocessing.py": 5522490547285108448, + "docs/api/html/group__ValidationFindingsPanel.html": 16524458792905973929, + "docs/api/html/group__GitEnvironmentRoutes.html": 2128130501766405947, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py": 5908944827805063108, + "frontend/src/lib/components/MaintenanceEventsTable.svelte": 1697386217337210174, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/__init__.py": 8434616194114282088, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py": 17221368219485351394, + "docs/api/html/group__test__tui__exit__f10.html": 6092888054021050903, + "docs/api/html/splitbard.png": 7867562212225872131, + "frontend/src/components/llm/ValidationReport.svelte": 17900215752976182981, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/licenses/LICENSE": 14316979437290727897, + "venv/lib/python3.13/site-packages/pygments/styles/tango.py": 12389250878249075539, + "docs/api/html/group__GitStatus.html": 12469885171313367595, + "docs/api/html/group__SupersetSqlLabExecutor.html": 4626317945232616372, + "docs/api/html/group____delete__stale__environment__records.html": 1017402888179208355, + "venv/lib/python3.13/site-packages/websockets/asyncio/client.py": 5694225754072131767, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arithmetic.py": 3209727509456212551, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/__init__.py": 2635182244050911910, + "venv/lib/python3.13/site-packages/authlib/oidc/core/models.py": 1370998944213578392, + "venv/bin/normalizer": 10972303485259566838, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/licenses/LICENSE": 5669644120528099197, + "venv/lib/python3.13/site-packages/pandas/util/_print_versions.py": 15326663505771588740, + "docs/api/html/group__auth__engine.html": 14325530296654107623, + "docs/api/html/group__test__repo__path__default__fixed.html": 5743804779893858892, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_threading.py": 6656552495241857519, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authenticate_client.py": 16886645731942472285, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/issue232.py": 6498198296113395367, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/WHEEL": 7314207500206292683, + "frontend/e2e/fixtures/global-setup.js": 16032945890934324203, + "docs/api/html/group__list__dictionary__entries.html": 12208362720304738846, + "frontend/playwright-report/trace/uiMode.Btcz36p_.css": 7361350017361398233, + "docs/api/html/group__GitMergeRoutes.html": 7022556547485727423, + "venv/lib/python3.13/site-packages/pygments/style.py": 17393199235575070210, + "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.pyi": 9291444594181042963, + "venv/lib/python3.13/site-packages/pandas/core/tools/datetimes.py": 13313374952742129866, + "frontend/build/_app/immutable/chunks/PPVm8Dsz.js": 5372483634308095655, + "docs/api/html/group__TranslationRunRetryManager.html": 16122389205545947641, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_indexing.py": 8943385096811168079, + "docs/api/html/group__GiteaRepoSchema.html": 16599628414846816204, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/constants.pyi": 15633065380510755171, + "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.pyi": 6723628093359375823, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_mips_unix.h": 17373389962405490482, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_series_equal.py": 1223890028205011021, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_pytables_missing.py": 12154582133989896069, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/emscripten_fetch_worker.js": 10530710876604536879, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/comparisons.pyi": 9338084426044982038, + "venv/lib/python3.13/site-packages/websockets/datastructures.py": 5180442040351387281, + "frontend/build/_app/immutable/chunks/BvHYGtTK.js": 8043469446863243101, + "frontend/build/_app/immutable/chunks/BVbOxB2s.js": 7751030861574170983, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth1_session.py": 1911069558956893789, + "backend/src/plugins/llm_analysis/plugin.py": 1790507864083708513, + "docs/api/html/group__test__mapping__service__alignment__with__test__data.html": 6791951592601504164, + "docs/api/html/group__PolicyResolutionService.html": 794248469337141330, + "docs/api/html/group__test__calculate__schedule__single__task.html": 12252381417874237319, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_editable.py": 12494407279914732338, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_freq_code.py": 13758353172962109140, + "backend/src/services/__tests__/test_llm_provider.py": 14345242680566878510, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2cmap.py": 5863341586534531594, + "docs/api/html/group__get__role__by__id.html": 5025478251031829538, + "docs/api/html/group__test__get__dashboard__detail__success.html": 2526597651975031803, + "examples/maintenance-api-bash.sh": 674396824183793987, + "backend/src/core/task_manager/cleanup.py": 13007785075566443785, + "backend/src/plugins/translate/metrics.py": 8214667548964093673, + "venv/lib/python3.13/site-packages/more_itertools/__init__.py": 9858612105173082060, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/scope.py": 4096190764836489091, + "venv/lib/python3.13/site-packages/pandas/io/parsers/python_parser.py": 7297059565749331767, + "venv/lib/python3.13/site-packages/numpy/random/mtrand.cpython-313-x86_64-linux-gnu.so": 16146459667121787202, + "venv/lib/python3.13/site-packages/anyio/_core/_exceptions.py": 11969894261631227715, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_pickle.py": 6232650893735715362, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_subclass.py": 16342917051908786497, + "docs/api/html/group__frontend__src__services__git__utils__js____buildSuggestedRepoName.html": 9912150894737317582, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/RECORD": 12575893084587070195, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop_duplicates.py": 2397396876872403048, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/setup.py": 5482352991573617756, + "venv/lib/python3.13/site-packages/pygments/lexers/elm.py": 14683437310498104916, + "docs/api/html/group__schedule__dashboard__validation.html": 18088681835965473182, + "venv/lib/python3.13/site-packages/pygments/lexers/grammar_notation.py": 9009644754106884095, + "frontend/build/_app/immutable/nodes/29.D30ywNG7.js": 1591159636347728189, + "docs/api/html/group__cancel__run.html": 14246191788643114054, + "docs/api/html/group____limited__user.html": 7899041144502275903, + "dist/docker/ss-tools-frontend.0.1.6.tar.xz": 10625748061118136131, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_indexing.py": 1947969642791009285, + "docs/api/html/group__TestClickHouseEndToEnd.html": 10329259316133049765, + "docs/api/html/group__update__yamls.html": 9301161878142210176, + "venv/lib/python3.13/site-packages/passlib/tests/test_context_deprecated.py": 7876919198015151018, + "backend/src/core/superset_client/_databases.py": 16562767538012813426, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_loop.py": 6502725909989655118, + "venv/lib/python3.13/site-packages/pandas/_libs/reshape.pyi": 88721900806893992, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isin.py": 14387850595546682314, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/scrypt.py": 15294662527620994511, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unstack.py": 11834917665446437163, + "venv/lib/python3.13/site-packages/pygments/lexers/gdscript.py": 12775803139854052838, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_period.py": 7727606662611927293, + "docs/api/html/group____get__environment__name__by__id.html": 16940554573354196856, + "docs/api/html/group__test__task__context__preserves__background__tasks__across__sub__context.html": 15827171381080992984, + "docs/api/html/group__EncryptionManager.html": 5675366914155455618, + "docs/api/html/group__SupersetClientHeaders.html": 659607831527775901, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_dispatcher.py": 2063037275809439611, + "docs/api/html/group__MappingsPageScript__Block.html": 3290002320641902083, + "docs/api/html/group__test__async__network__404__mapping__translates__dashboard__endpoints.html": 11153828192989017659, + "venv/lib/python3.13/site-packages/sqlalchemy/event/registry.py": 2857989070743633709, + "venv/lib/python3.13/site-packages/git/objects/submodule/util.py": 8621897576747443758, + "docs/api/html/group__SupersetClientGetNativeFilterState.html": 7632236885296955181, + "venv/lib/python3.13/site-packages/pip/_internal/utils/egg_link.py": 18211947508793805393, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_expanding.py": 9047466276603454007, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_map.py": 6556132946198785761, + "docs/api/html/group__PromoteResponse.html": 6455148757266042889, + "venv/lib/python3.13/site-packages/cffi/error.py": 8147505969912539538, + "venv/lib/python3.13/site-packages/pandas/core/methods/to_dict.py": 4653628436393663485, + "backend/src/schemas/validation.py": 3700985772197763306, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____endEvent.html": 8695734456482640318, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_salsa.py": 1756536674254931838, + "docs/api/html/group__SupersetDashboardsWriteMixin.html": 8613916271231427973, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/__init__.py": 5896924546560667519, + "docker/nginx.conf": 14595959129793115016, + "venv/lib/python3.13/site-packages/greenlet/tests/test_throw.py": 4119265751381210399, + "docs/api/html/group__handleEnvChange__Function.html": 18099502322897430177, + "venv/lib/python3.13/site-packages/jeepney/tests/test_low_level.py": 3579024079580951234, + "venv/lib/python3.13/site-packages/authlib/oidc/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/testing.pyi": 9317922890783549326, + "venv/lib/python3.13/site-packages/pygments/lexers/gleam.py": 5161116162065925222, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayprint.py": 5912583751693302370, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ma.pyi": 8186102816080813799, + "backend/src/plugins/translate/_lang_detect.py": 2125979777533490081, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_pickle.py": 16196792378104396231, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.cpython-313-x86_64-linux-gnu.so": 12776951671822017500, + "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key.html": 10269943654735165789, + "docs/api/html/group__test__api__key__auth__wrong__permission.html": 546528114053680205, + "backend/src/plugins/translate/dictionary_correction.py": 18051915404774774954, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_indexing.py": 7181087459615188551, + "backend/ss_tools_backend.egg-info/PKG-INFO": 15134841326777953914, + "venv/lib/python3.13/site-packages/pillow.libs/libbrotlicommon-c55a5f7a.so.1.2.0": 2635192149914499986, + "docs/api/html/group__test__database__url__postgres__fallback.html": 15099231812721342773, + "docs/api/html/group__prepare__candidate__legacy.html": 17583352627738261078, + "docs/api/html/group____normalize__term.html": 2393952213994759345, + "docs/api/html/group__SupersetCompilationAdapter______init____.html": 4108260053223346394, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pipe.py": 7400355977900310233, + "docs/api/html/group__GitPluginExt.html": 959273690760699620, + "docs/api/html/group__SemanticCandidateDto.html": 8345714029489637620, + "docs/api/html/group__JobLifecycleModule.html": 9294657013529103257, + "docs/api/html/group__TargetSchemaValidationResponse.html": 4850993964475160143, + "docs/api/html/group__calculate__crc32.html": 15503083199679692336, + "venv/lib/python3.13/site-packages/pygments/scanner.py": 5640381644804445932, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/util.py": 18279366777506483176, + "venv/lib/python3.13/site-packages/passlib/ext/__init__.py": 15240312484046875203, + "docs/api/html/group__SupersetContextRecoveryMixin__recover__imported__filters.html": 12238037289621138113, + "docs/api/html/group__MaintenanceOrchestrators.html": 18097597528748465937, + "docs/api/html/group__ProfilePreferenceService.html": 9611968709231329554, + "docs/api/html/group__test__allowed__origins__parsing.html": 12390096577494505113, + "venv/lib/python3.13/site-packages/pydantic/generics.py": 15019381413806459410, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/METADATA": 18410956221497090898, + "backend/src/services/clean_release/preparation_service.py": 9259410398708054305, + "venv/lib/python3.13/site-packages/pygments/lexers/savi.py": 95400050302239274, + "backend/tests/test_log_persistence.py": 4939639817358494977, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/METADATA": 14543822270526146783, + "docs/api/html/group__test__backup__dashboards__with__schedule.html": 11599542107970767027, + "docs/api/html/group__ValidationRunDetailResponse.html": 2243954401431072205, + "docs/api/html/group__normalize__owner__tokens.html": 10173921796259136968, + "docs/api/html/group__get__validation__policies.html": 18157978184513887167, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/AUTHORS": 3558407083075395848, + "venv/lib/python3.13/site-packages/pandas/core/ops/common.py": 17314999825118928100, + "docs/api/html/group__test__add__logs__batch.html": 7044990428068455797, + "docs/api/html/group____resolve__current__user__git__identity.html": 9945081661871337184, + "venv/lib/python3.13/site-packages/pandas/tests/extension/date/__init__.py": 16789767433439616593, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas_xport.py": 16525568795475918874, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/endpoint.py": 13389763148720370546, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/__main__.py": 2688850704829116818, + "docs/api/html/group__test__rejectRow__Function.html": 14197518524874918963, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_complex.py": 12963449957363121481, + "docs/api/html/group__CotLoggerModule.html": 10976879847879957078, + "docs/api/html/group__test__missing__auth__db__url__crashes.html": 1426005486065410823, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_append.py": 9644955836191900614, + "docs/api/html/group____get__task__service.html": 7652976387374733234, + "docs/api/html/navtreeindex25.js": 18335817056234416468, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/memmap.pyi": 15629663272474054909, + "docs/api/html/group__test__report__builder__sets__immutable__snapshot__flag.html": 4818862330013157215, + "backend/src/core/task_manager/context.py": 11799776870777277331, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/__init__.py": 14522970260621605208, + "docs/api/html/group__BulkReplacePreviewItem.html": 14430839695131325196, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py": 2763962019423787504, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_store.py": 1500300322083708794, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/json.py": 15085143436056608404, + "venv/lib/python3.13/site-packages/click/shell_completion.py": 5858110451169927373, + "venv/lib/python3.13/site-packages/pygments/lexers/codeql.py": 4254046699692048717, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__GitServerConfig.html": 770018572005176165, + "venv/lib/python3.13/site-packages/rapidfuzz/_common_py.py": 15410157970973169427, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_at_time.py": 15352346149584081864, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_collections.py": 7772437080414835462, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____updateSettings.html": 7369380578343660660, + "venv/lib/python3.13/site-packages/pandas/_libs/lib.pyi": 3049004444338566197, + "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.pyi": 13945478633722616250, + "venv/lib/python3.13/site-packages/anyio/abc/_eventloop.py": 2023190981288767991, + "docs/api/html/group____extract__chart__data__rows.html": 2508559388931054401, + "docs/api/html/group__clear__tasks.html": 2396216755945302765, + "docs/api/html/group__updateLoggingConfig__Function.html": 5025211689617667851, + "docs/api/html/group__AssistantSchemas.html": 17488044216136493605, + "venv/lib/python3.13/site-packages/keyring/__init__.py": 991260689223167957, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_parquet.py": 11050150015772440923, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.pyi": 1521384724035973688, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_isin.py": 11330491145602008796, + "venv/lib/python3.13/site-packages/pandas/core/indexes/range.py": 3838277376774246164, + "venv/lib/python3.13/site-packages/pygments/lexers/asn1.py": 15030988816882821134, + "docs/api/html/group__test__get__datasets__invalid__pagination.html": 12210772890706448571, + "venv/lib/python3.13/site-packages/numpy/matlib.pyi": 8017902013455539981, + "docs/api/html/group__test__belief__scope__logs__reason__reflect__at__debug.html": 16218626126830450612, + "venv/lib/python3.13/site-packages/psycopg2/__init__.py": 15798412693009721199, + "docs/api/html/group__normalize__answer__value.html": 5431564849732382081, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/base.py": 3250660921154829815, + "docs/api/html/group__test__get__dashboards__search.html": 3310167705185192506, + "docs/api/html/group____matches__dashboard__actor__aliases.html": 12171503296402923897, + "venv/lib/python3.13/site-packages/passlib/exc.py": 15559705487889657091, + "venv/lib/python3.13/site-packages/pandas/tests/test_downstream.py": 5635510753354256507, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_setops.py": 16240008859650640717, + "docs/api/html/group__test__error__state.html": 9960175687355518879, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/integration.py": 12670848015915724967, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets2.py": 11100501798741918610, + "docs/api/html/group__test__end__event__shared__banner.html": 116805575152722056, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_routines.py": 4860173574365060471, + "docs/api/html/group__test__get__remote__ids__batch__returns__empty__for__empty__input.html": 5603538648054911229, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_impl.py": 12056958946481531279, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_datetime.py": 4179112383995346046, + "docs/api/html/group__test__patch__dashboard__metadata__replaces__chart__ids.html": 13396557809840355456, + "backend/src/services/clean_release/demo_data_service.py": 14506634271910590558, + "docs/api/html/globals_s.html": 14802100612037968134, + "venv/lib/python3.13/site-packages/PIL/JpegImagePlugin.py": 10621639599916796689, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_constructors.py": 6440526981746306640, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex.tpl": 8820045022551446255, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/formatters/pangomarkup.py": 17135620955205945255, + "docs/api/html/group__test__create__task__invalid__provider.html": 13091245729533197947, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_accumulations.py": 11428556645065711062, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_api.py": 5287018911951723034, + "venv/lib/python3.13/site-packages/pygments/lexers/_postgres_builtins.py": 2383906454995727700, + "frontend/src/lib/components/reports/__tests__/fixtures/reports.fixtures.js": 9590309593907049070, + "venv/lib/python3.13/site-packages/smmap/buf.py": 18231775424238352734, + "docs/api/html/group__test__patch__dashboard__metadata__replaces__dataset__ids.html": 1839137190249919532, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_wrap.py": 17083394123203662880, + "docs/api/html/group__DuplicateJobResponse.html": 4274099317404144755, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_clipboard.py": 12674078499544938252, + "docs/api/html/group__getSuggestion__Function.html": 1195438535838483497, + "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.cpython-313-x86_64-linux-gnu.so": 3478629877770507853, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/constrain.py": 6513876973118703677, + "frontend/src/routes/reports/+page.svelte": 18404964858248487921, + "docs/api/html/group__test__multiple__events.html": 15612673565402865625, + "docs/api/html/group__SidebarTestModule.html": 15647744772485755211, + "venv/lib/python3.13/site-packages/rsa/common.py": 10921682819204792805, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/adapters.py": 2224938661429637508, + "docs/api/html/group__test__except__pass__replaced__with__log.html": 2189835448945118208, + "docs/api/html/group__test__fetchPreview__Function.html": 9834489208067196451, + "venv/lib/python3.13/site-packages/passlib/crypto/des.py": 14699826058580193499, + "backend/src/services/clean_release/repositories/publication_repository.py": 1097065694003039928, + "docs/api/html/group__DashboardHeader.html": 15889628844920899233, + "docs/api/html/group__REASONING__OVERHEAD.html": 6648865310859009739, + "docs/api/html/group__TaskRecord.html": 17484476379866416581, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate.py": 16723865897349422996, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE": 16499431172938528491, + "docs/api/html/group__test__git__service__init__repo__reclones__when__path__is__not__a__git__repo.html": 3825554148900324930, + "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.pyi": 5786150108623563787, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_scalar_compat.py": 10534925655070162459, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_constructors.py": 11786151805350138436, + "docs/api/html/group__mock__deps.html": 1002676967209626799, + "venv/lib/python3.13/site-packages/pydantic/deprecated/parse.py": 819235411736647763, + "venv/lib/python3.13/site-packages/pygments/lexer.py": 3658450949169647228, + "docs/api/html/group__DashboardChartItem.html": 6833781467610303476, + "venv/lib/python3.13/site-packages/_pytest/pastebin.py": 13946273207624603063, + "venv/lib/python3.13/site-packages/pip/_internal/cli/status_codes.py": 9143625681947330756, + "venv/lib/python3.13/site-packages/pycparser/ply/cpp.py": 1665654017894222868, + "venv/lib/python3.13/site-packages/pandas/tests/tools/__init__.py": 15130871412783076140, + "docs/api/html/group__SupersetCompilationAdapter.html": 17117078588254804652, + "docs/api/html/group__src__core.html": 15005492864993881989, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/dml.py": 6519732236745017861, + "venv/lib/python3.13/site-packages/dotenv/version.py": 13508433229287636267, + "docs/api/html/group__extractDatasetReviewVersion__Function.html": 18233279301174300230, + "venv/lib/python3.13/site-packages/pandas/tests/api/test_types.py": 8889829773923216769, + "venv/lib/python3.13/site-packages/pygments/styles/lovelace.py": 15082819162722685616, + "docs/api/html/group__test__detect__virtual__columns.html": 7581525979923931052, + "venv/lib/python3.13/site-packages/pygments/lexers/supercollider.py": 15306357945850389082, + "venv/lib/python3.13/site-packages/numpy/_core/printoptions.pyi": 16758392990920041462, + "backend/src/schemas/dataset_review_pkg/_composites.py": 16155371874529336756, + "docs/api/html/group__HealthService.html": 2595257407223931546, + "venv/lib/python3.13/site-packages/numpy/_core/_asarray.pyi": 3940937389249665490, + "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.pyi": 18290764713161282980, + "docs/api/html/group__ValidationRoutes.html": 9947226758890063064, + "docs/api/html/group__ROUTES__DIR.html": 11229403533330790472, + "docs/api/html/group__update__execution__mapping.html": 5866096724549130972, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/recfunctions.py": 18330899924171767482, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/aioodbc.py": 2018840826888846321, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/base.py": 7110726987481713080, + "venv/lib/python3.13/site-packages/keyring/util/__init__.py": 9635016046220094473, + "docs/api/html/group__TranslateScheduleRoutesModule.html": 6135158019690701715, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/retry.py": 17371295199208325375, + "docs/api/html/minusd.svg": 1684399045313528301, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_libgroupby.py": 9985769511561272200, + "venv/lib/python3.13/site-packages/pandas/tests/internals/test_api.py": 11217428888457119601, + "backend/src/plugins/translate/preview_llm_client.py": 6145483054507946023, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/flatiter.py": 3066469329738423021, + "backend/src/api/routes/git/_gitea_routes.py": 3966408809424419232, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/provision.py": 2676876852726658063, + "docs/api/html/group__AssistantHistory.html": 5407469429685717474, + "docs/api/html/group__AssistantModels.html": 13818196745867110031, + "venv/lib/python3.13/site-packages/yaml/resolver.py": 6669674521776449098, + "docs/api/html/group__end__maintenance.html": 13575739036759149752, + "docs/api/html/group__handleDeleteEnv__Function.html": 14510731287987560462, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/__init__.pyi": 15820654525292895396, + "docs/api/html/group____check__translation__cache.html": 3321256720514303969, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/assignOnlyModule.f90": 15967199601254912096, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/licenses/LICENSE": 9235234428907927646, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/greenlet/TThreadStateDestroy.cpp": 13749797532421836328, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.pyi": 10947021928755741366, + "frontend/src/routes/datasets/__tests__/split_view.test.js": 6143972529940917004, + "backend/src/services/clean_release/stages/no_external_endpoints.py": 5083253025896929417, + "docs/api/html/group__StartSessionResult.html": 15571965797483331043, + "venv/lib/python3.13/site-packages/pip/_internal/network/__init__.py": 1586148254475575526, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/__init__.py": 16532856661318424631, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/greenlet/slp_platformselect.h": 10428201768098464386, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_ndarray_backed.py": 10097812802469682233, + "docs/api/html/group__DebounceModule.html": 8591276512225370990, + "docs/api/html/group__test__tui__run__checks__f5.html": 11086893283846655051, + "docs/api/html/group__end__all__maintenance.html": 3898926746936500242, + "docs/api/html/group__orchestrator__config.html": 16901111520740994959, + "docs/api/html/group____flusher__loop.html": 18087124842264758700, + "venv/lib/python3.13/site-packages/passlib/ext/django/utils.py": 8118896525895185626, + "venv/bin/fastapi": 12400009726251862770, + "venv/lib/python3.13/site-packages/pygments/lexers/web.py": 11104721221615804497, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_nonunique_indexes.py": 15468078561381939405, + "frontend/src/lib/stores/assistantChat.js": 16716458796178537885, + "venv/lib/python3.13/site-packages/pycparser/c_ast.py": 5942107079119742, + "docs/api/html/group__test__string__literal__false__positive.html": 7083542074508752106, + "venv/lib/python3.13/site-packages/pydantic/experimental/pipeline.py": 9493054841465227788, + "docs/api/html/group__TranslateMetricsRoutesModule.html": 13065795937255941504, + "docs/api/html/group__EventBusModule.html": 12486786797945687836, + "docs/api/html/group__PermissionDeniedError.html": 13284579335142541515, + "venv/lib/python3.13/site-packages/numpy/_core/records.py": 11414828908088050950, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_indexing.py": 16291207621268530224, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/scalars.pyi": 3130685119595923378, + "venv/lib/python3.13/site-packages/pygments/styles/pastie.py": 15784474766258831882, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeSupersetBaseUrl.html": 9936225426251260789, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/type_check.pyi": 1599910480066368464, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_online.py": 11850497659869773510, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_extending.py": 10202091354750153497, + "venv/lib/python3.13/site-packages/pandas/io/formats/style.py": 13235403689320479165, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop_duplicates.py": 5874427993953991216, + "docs/api/html/group__getRepositoryStatusToken__Function.html": 49346559759034535, + "docs/api/html/group__SrcRoot.html": 9966390518366512890, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/installed.py": 18153165899841229597, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/appengine.py": 16194997685865850655, + "venv/lib/python3.13/site-packages/pandas/_testing/contexts.py": 11714205921752896137, + "frontend/build/_app/immutable/chunks/D_lzkvUl.js": 360424679079824486, + "frontend/src/routes/settings/notifications/+page.svelte": 11507547877108528071, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-1.csv": 9129730764673472086, + "venv/lib/python3.13/site-packages/cryptography/x509/extensions.py": 10197248868682803019, + "docs/api/html/navtree.css": 8480429594731608749, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log10.csv": 13441294827017400897, + "docs/api/html/group__EnvironmentNotFoundError.html": 10410223898319526966, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/fastapi/logger.py": 11590581556317053213, + "docs/api/html/group__SingleConfigDbMock.html": 1705315936176408905, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_assumed_shape.py": 18361009188431483164, + "backend/src/plugins/__init__.py": 17194112070692432889, + "docs/api/html/globals_func_d.html": 802653481815391766, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/hotp.py": 18325640697829098192, + "docs/api/html/group__should__log__task__level.html": 3031004604954435079, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_openssl.py": 10446441599800965961, + "venv/lib/python3.13/site-packages/PIL/WmfImagePlugin.py": 15685062825106444247, + "docs/api/html/group__test__page__size__constant__exists.html": 5309175304176573078, + "venv/lib/python3.13/site-packages/pandas/_libs/pandas_datetime.cpython-313-x86_64-linux-gnu.so": 15874094681262240628, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/instrumentation.py": 13198832283145198959, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/traversals.py": 1188980668212022868, + "venv/lib/python3.13/site-packages/packaging/_structures.py": 13086687542872305890, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunc_config.pyi": 11754104063443791746, + "docs/api/html/group__auth__service.html": 15607649240314134855, + "docs/api/html/group__test__us2__field__semantic__override__lock__unlock__and__feedback.html": 7173438708880743570, + "docs/api/html/search/all_8.js": 12526514117629582170, + "docs/api/html/group__reject__candidate.html": 4888870338363798729, + "scripts/scan_secrets.sh": 10827095619853567115, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/background.py": 2105814165112049750, + "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.pyi": 15767546107856827543, + "venv/lib/python3.13/site-packages/pygments/lexers/modeling.py": 15714077241620129689, + "docs/api/html/group__TaskManagerModule.html": 10905637838927544085, + "docs/api/html/group__taskDrawer__Store.html": 6664598723983358111, + "docs/api/html/group__LoginPage.html": 5394223316995061464, + "docs/api/html/group____serialize__execution__mapping.html": 14633172635644173618, + "venv/lib/python3.13/site-packages/pandas/_libs/algos.pyi": 22405377601029952, + "venv/lib/python3.13/site-packages/pandas/tests/computation/test_compat.py": 11655000938111420543, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/relationships.py": 8401569216905291119, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/totp.py": 11888568940387752607, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____readTabFromUrl.html": 15117558677865436806, + "backend/src/schemas/settings.py": 17501571983262614937, + "docs/api/html/group__fetch__source__rows.html": 5710393657520357008, + "docs/api/html/search/functions_11.js": 18073114919847941486, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/screen.py": 6933591906297023845, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas.py": 9446226619493404132, + "docs/api/html/group__DictionaryDetailPage.html": 4983283205838955822, + "docs/api/html/group__MappedFields.html": 12079507314876337159, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_upcast.py": 6496794053681247105, + "docs/api/html/group__wait__for__resolution.html": 4445289133815184420, + "docs/api/html/group__test__get__users__page__uses__fallback__endpoint__when__primary__fails.html": 476657279500282456, + "venv/lib/python3.13/site-packages/pycparser/ast_transforms.py": 9032453625977464506, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-np0-objarr.npy": 1615546603475261360, + "docs/api/html/group__test__recent__pending__run__not__stale.html": 3405692392212130999, + "docs/api/html/group__Sidebar.html": 9352378737121268807, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix_py.py": 17041764258638228993, + "venv/lib/python3.13/site-packages/websockets/legacy/__init__.py": 12332512329533591682, + "docs/api/html/group__EncryptionKeyModule.html": 10145923645674172016, + "docs/api/html/group__APIKeyModel.html": 7167590347437218348, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_find_common_type.py": 9581516171321396333, + "backend/alembic/versions/c4a3a2f74bfe_add_missing_columns_needs_review_.py": 9348403245223015273, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connectionpool.py": 10453724680327835821, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/table.py": 16856253349556498073, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/filepost.py": 16698380375728063923, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_two_greenlets.py": 11156275429806391390, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dtype.py": 5553691086633215767, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo77.f": 7353666717928699883, + "docs/api/html/tabs.css": 16739738225401062757, + "docs/api/html/group__add__log.html": 5531300137956566412, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE": 12064892310002036996, + "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiDelete.html": 3340000838366593929, + "docs/api/html/group__test__get__report__detail__success.html": 15353646901351166916, + "docs/api/html/group__TestSupersetProfileLookup.html": 8558198565478717735, + "docs/api/html/group__test__json__load__if__needed__list.html": 793875230537315255, + "docs/api/html/group__getLoggingConfig__Function.html": 3509641423397753733, + "docs/api/html/group__FileCategory.html": 1512310133619337976, + "docs/api/html/group__test__very__long__content.html": 5572816377205062437, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py": 2516476609539242190, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_formats.py": 12299393760307251036, + "venv/lib/python3.13/site-packages/pip/_internal/utils/appdirs.py": 539688259643447487, + "backend/src/plugins/llm_analysis/models.py": 92649032056187888, + "docs/api/html/group__get__candidate__overview__v2__endpoint.html": 3653749146753267302, + "docs/api/html/group__frontend__src__services__git__utils__js____extractUnfinishedMergeContext.html": 6678834570303525459, + "docs/api/html/group__AuthOauthModule.html": 13530771519498131347, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_version.pyi": 18093228021414910594, + "docs/api/html/group__test__cors__wildcard__default.html": 5002966097368002344, + "venv/lib/python3.13/site-packages/numpy/random/tests/__init__.py": 15130871412783076140, + "docs/api/html/group__ensure__initial__admin__user.html": 9396064717758252856, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/token.py": 4161760290367932793, + "docs/api/html/search/functions_3.js": 415719142770932975, + "venv/lib/python3.13/site-packages/httpx/_status_codes.py": 1758440600462491839, + "venv/lib/python3.13/site-packages/numpy/_pytesttester.py": 17737911110576747508, + "venv/lib/python3.13/site-packages/pandas/core/arrays/categorical.py": 15993211255162930859, + "backend/src/core/mapping_service.py": 5863886462891122323, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/string.f": 17328443345813567449, + "backend/src/plugins/translate/preview_executor.py": 11680052270975606699, + "frontend/e2e/run-e2e.sh": 8106714116467101372, + "docs/api/html/group__test__register__candidate__rejects__duplicate__candidate__id.html": 6105187651633201559, + "docs/api/html/group__test__fetchPreviewRecords__Function.html": 3636775598407887299, + "venv/lib/python3.13/site-packages/passlib/handlers/cisco.py": 15004707325761459775, + "docs/api/html/group__test__release__reject__contract.html": 11500420123173892128, + "backend/src/services/git/_base.py": 5742931931968617419, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_css.py": 17765129030290410902, + "docs/api/html/group__serve__spa.html": 411901662117406287, + "docs/api/html/group__list__dictionaries.html": 17979156195580437161, + "venv/lib/python3.13/site-packages/git/refs/symbolic.py": 14996822770181935589, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py": 13206297306768491360, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi": 13016654355866474644, + "venv/lib/python3.13/site-packages/pandas/core/internals/__init__.py": 1377108808392195313, + "docs/api/html/group__DatasetRunContextDto.html": 17960720894742072910, + "docs/api/html/group__test__recover__imported__filters__collapses__state__and__metadata__duplicates__into__one__canonical__filter.html": 4764189400381267919, + "venv/lib/python3.13/site-packages/pygments/lexers/email.py": 4355246279485460399, + "venv/lib/python3.13/site-packages/fastapi/__init__.py": 11054203163075417954, + "venv/lib/python3.13/site-packages/bcrypt/__about__.py": 11196369962175692786, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_functions.py": 16376319335326812580, + "backend/src/plugins/translate/__tests__/test_orchestrator.py": 10313792176402719801, + "venv/lib/python3.13/site-packages/numpy/tests/test_scripts.py": 5790352482583964348, + "venv/lib/python3.13/site-packages/pygments/lexers/qlik.py": 3881663935630350152, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/multiarray.pyi": 6368268452184570195, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/nonce.py": 18219168479887197733, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/shape.py": 13410030168622484801, + "venv/lib/python3.13/site-packages/pygments/styles/friendly_grayscale.py": 16840106759769837649, + "backend/tests/services/clean_release/test_publication_service.py": 5088943335705207828, + "docs/api/html/group__ManifestRepository.html": 9159461313767192573, + "frontend/src/lib/stores/health.js": 3621667160018048028, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py": 15060063324909302240, + "backend/alembic/versions/f0e9d8c7b6a5_cascade_ondelete_all_env_fks.py": 9305001292096797070, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/httpcore/_sync/interfaces.py": 1532245423878428608, + "docs/api/html/group__test__delete__logs__for__tasks__empty.html": 11042736857003485976, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reshape.py": 10576230966727810832, + "venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py": 8698341051522816167, + "docs/api/html/group__SemanticFieldEntry.html": 12684503746911343519, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_concat.py": 4472503149811617440, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo.f": 5690596225005490325, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_policy.py": 13856626567322091253, + "docs/api/html/group__call__openai__compatible.html": 5568369277413456833, + "docs/api/html/group__test__stale__pending__cleared__and__proceeds.html": 15143795866405966436, + "venv/lib/python3.13/site-packages/pandas/_libs/internals.cpython-313-x86_64-linux-gnu.so": 2008633361686956377, + "docs/api/html/group__pull__changes.html": 14394506441825881379, + "docs/api/html/nav_f.png": 2704451415722649096, + "venv/lib/python3.13/site-packages/numpy/ma/testutils.py": 9355402483340137461, + "docs/api/html/group__frontend__src__services__git__utils__js____applyGitflowStageDefaults.html": 10826927969679877530, + "venv/lib/python3.13/site-packages/passlib/handlers/pbkdf2.py": 16686685453577026643, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py": 15726448028057859831, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_fillna.py": 13675584998644907265, + "venv/lib/python3.13/site-packages/sqlalchemy/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_array.py": 16867692337986161626, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_musllinux.py": 2423350951094915747, + "venv/lib/python3.13/site-packages/pandas/tests/test_sorting.py": 10751098438703044961, + "backend/src/plugins/migration.py": 10457528901248546685, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dtype.py": 5336980126536664360, + "backend/src/plugins/search.py": 13705421801923369220, + "docs/api/html/group__test__submit__correction__conflict__detected.html": 13261203944442671395, + "docs/api/html/group__SlackProvider.html": 13046100064758196543, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/__init__.py": 15130871412783076140, + "docs/api/html/group__extract__owner__identifiers.html": 8294813531215300582, + "venv/lib/python3.13/site-packages/keyring/testing/backend.py": 14295829193445781542, + "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_version.py": 849232015288488204, + "docs/api/html/group__DatasetProfile.html": 5331057226299357274, + "venv/lib/python3.13/site-packages/pandas/__init__.py": 793698514004377486, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/RECORD": 12555714734722889695, + "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js": 18193433457755419816, + "docs/api/html/group__ErrorContext.html": 3882007036049607733, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_frozen.py": 3484963987053884566, + "venv/lib/python3.13/site-packages/pydantic/plugin/__init__.py": 16803973597469216174, + "docs/api/html/group__AggregatedMetricsResponse.html": 5296336191776764582, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Prefix.pyi": 2700568594438441246, + "backend/git_repos/remote/test-repo.git/objects/c2/491dfd8c841bacd5f491510f6334aa4a626f00": 6297303620364618482, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pop.py": 5700439923973346681, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/__init__.py": 13841777369501476946, + "docs/api/html/globals_p.html": 5091452083134997558, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/METADATA": 14036738037171597049, + "docs/api/html/group__PreviewSessionManager.html": 4001497686990395756, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/transforms.py": 3773159138960141434, + "venv/lib/python3.13/site-packages/pandas/compat/pickle_compat.py": 11073483984394766474, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildAssistantContextPrompt.html": 7622914159072277055, + "venv/lib/python3.13/site-packages/pygments/lexers/tact.py": 1164971357219795420, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite_e.py": 8912795372542950449, + "venv/lib/python3.13/site-packages/pip/_internal/pyproject.py": 6155748929233245415, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval_pyarrow.py": 3849382723890414899, + "backend/src/api/routes/assistant/_tool_llm_validation.py": 114694857293212677, + "docs/api/html/group__TestSettingsAndHealthSchemas.html": 3643400458745019094, + "docs/api/html/group__ProfilePermissionState.html": 771733666078465816, + "docs/api/html/group__ADGroupMappingSchema.html": 11869729536945519142, + "venv/lib/python3.13/site-packages/_pytest/config/__init__.py": 12787579251203510616, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/rec.pyi": 14615751247243515028, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/abstract.py": 14216912708889351810, + "backend/git_repos/remote/test-repo.git/refs/heads/feature/test": 7714224145564125988, + "docs/api/html/group__get__password__hash.html": 1918521256255854564, + "venv/lib/python3.13/site-packages/_pytest/hookspec.py": 387790426108810884, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_matmul.py": 9323220618335072725, + "docs/api/html/group__Utils__Module.html": 1214868907405242018, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_get_dummies.py": 16112674981222844375, + "docs/api/html/group__ReadinessState.html": 18424054850820487060, + "venv/lib/python3.13/site-packages/rapidfuzz/utils_py.py": 12341806772632276946, + "venv/lib/python3.13/site-packages/PIL/ImageMorph.py": 13861961228125789866, + "docs/api/html/group____run__to__response.html": 4944388567764979533, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/ocsp.pyi": 8666343427862784832, + "backend/src/plugins/translate/dictionary.py": 2784152095977907504, + "venv/lib/python3.13/site-packages/ecdsa/ecdsa.py": 9770689913380927623, + "venv/lib/python3.13/site-packages/passlib/registry.py": 5050675058011850564, + "venv/lib/python3.13/site-packages/PIL/ImageStat.py": 18442709639217312689, + "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.py": 7352635707860427145, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_formats.py": 9738552308554950419, + "frontend/playwright-report/trace/assets/defaultSettingsView-D31xz8zv.js": 1301407320635785789, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_insert.py": 11635164392966270621, + "venv/lib/python3.13/site-packages/pillow.libs/libharfbuzz-0692f733.so.0.61230.0": 23110287346167671, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_skew_kurt.py": 15225601437354712351, + "backend/src/api/routes/__tests__/test_assistant_api.py": 15369889927173527259, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_formats.py": 2601044064677727807, + "docs/api/html/group__ImportArtifactsRequest.html": 9466947930245217230, + "docs/api/html/group__test__load__tasks.html": 16906154469503701361, + "docs/api/html/group__start__session.html": 8274556149199075536, + "docs/api/html/group__test__new__key__only__mode.html": 7530821276868419489, + "docs/api/html/group__test__get__schedule__not__found.html": 2262227817332764614, + "docs/api/html/group__TraceContextMiddleware______init____.html": 1201832506877617610, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/__init__.py": 4752355175318979033, + "docs/api/html/group__test__complete__state.html": 10840390930299689847, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/__init__.py": 1343779312297539596, + "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.pyi": 3814281717779245045, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpq-9b38f5e3.so.5.17": 9680474575710585826, + "docs/api/html/group__AssistantAdminRoutes.html": 8552062072731347537, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/dtypes.py": 9839461501970023861, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_arithmetic.py": 17427754419626622853, + "backend/tests/test_logging_audit_fixes.py": 5356101714060259249, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_formats.py": 18080627205081311773, + "frontend/src/components/tasks/LogFilterBar.svelte": 1342324464094667837, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/METADATA": 15888766579203341778, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_conversion.py": 12184280074522274780, + "venv/lib/python3.13/site-packages/pygments/lexers/j.py": 9766594937623918053, + "venv/lib/python3.13/site-packages/authlib/oidc/core/util.py": 5364455058429522633, + "venv/lib/python3.13/site-packages/pydantic/schema.py": 83238111330132033, + "venv/lib/python3.13/site-packages/typing_inspection/__init__.py": 15130871412783076140, + "docs/api/html/group__SupersetClientExportDashboard.html": 9407120539221335337, + "docs/api/html/group____ensure__dictionary__entries__columns.html": 2292525621373712406, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/base.py": 5924592154431932923, + "venv/lib/python3.13/site-packages/httpcore/_sync/connection.py": 1539337414493486168, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/__init__.py": 15991623390785635773, + "venv/lib/python3.13/site-packages/pandas/_libs/properties.pyi": 9268470215750269699, + "docs/api/html/group__approve__execution__mapping.html": 11553995581837214740, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/__init__.py": 4014740126161973332, + "venv/lib/python3.13/site-packages/authlib/jose/__init__.py": 317573635113539563, + "venv/lib/python3.13/site-packages/PIL/XbmImagePlugin.py": 12256750478268246137, + "venv/lib/python3.13/site-packages/cffi/verifier.py": 8473599757770867754, + "backend/git_repos/remote/test-repo.git/objects/6a/f2b28e314c04fb4e03476c12d6491213591512": 4526311953587849385, + "frontend/src/components/DynamicForm.svelte": 2120031570302269345, + "venv/lib/python3.13/site-packages/click/_textwrap.py": 14586282603215052310, + "venv/lib/python3.13/site-packages/tzlocal/utils.py": 7893867974548929154, + "venv/lib/python3.13/site-packages/jeepney/fds.py": 16573439672758641560, + "venv/lib/python3.13/site-packages/gitdb/const.py": 14122142034887286559, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/validation": 13992741783834278677, + "venv/lib/python3.13/site-packages/multipart/multipart.py": 26386459953377432, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/METADATA": 15658158392301945427, + "docs/api/html/group__ToastsModule.html": 15194540860991792583, + "venv/lib/python3.13/site-packages/fastapi/dependencies/models.py": 8626894872248212015, + "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.py": 10803011906582324302, + "docs/api/html/group__has__plugin.html": 2360443021065095729, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex_like.py": 13803156447991282315, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.obj": 11967637259540485652, + "venv/lib/python3.13/site-packages/cffi/setuptools_ext.py": 30796527302561458, + "docs/api/html/group__test__add__and__remove__translation__job.html": 2228611743163264158, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_tokenizer.py": 7994281991455916540, + "docs/api/html/group__TaskDrawerStoreTest__Module.html": 12333000150269624051, + "docs/api/html/group__test__get__all__metrics.html": 16314403640662640562, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_style.tpl": 3352086677417301397, + "venv/lib/python3.13/site-packages/cffi/ffiplatform.py": 6782148110863549472, + "docs/api/html/group__ui__Module.html": 3556271867573996475, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed25519.pyi": 16312114056447366970, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/__init__.py": 13331622784664204066, + "venv/lib/python3.13/site-packages/numpy/lib/user_array.py": 14003456655595459585, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_timezones.py": 15157687716502574038, + "docs/api/html/group__required__permission.html": 10075560287490873276, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/util.py": 3591114320020964752, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tz_localize.py": 13699016868476153393, + "venv/lib/python3.13/site-packages/_pytest/setupplan.py": 7496801885995567141, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/dynamic.py": 15220116196072524330, + "venv/lib/python3.13/site-packages/pandas/core/__init__.py": 15130871412783076140, + "frontend/src/lib/ui/Icon.svelte": 18238812161544525261, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/distro.py": 12494639376332486204, + "docs/api/html/group__get__payload.html": 3413926737425743336, + "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.pyi": 2029639875417260268, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.pyi": 4432290176776357346, + "docs/api/html/group__handleCreateBackup__Function.html": 6719288563421799770, + "docs/api/html/group__docker__frontend__entrypoint.html": 16625957317311694585, + "venv/lib/python3.13/site-packages/anyio/_core/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/xcode.py": 14124729477757765388, + "backend/src/schemas/__tests__/test_settings_and_health_schemas.py": 17933166680828315099, + "venv/lib/python3.13/site-packages/fastapi/types.py": 3867821246650502267, + "frontend/src/components/MappingTable.svelte": 16339628752309005366, + "docs/api/html/group__SupersetContextTemplatesMixin__discover__template__variables.html": 1854980804207129510, + "backend/tests/services/clean_release/test_approval_service.py": 1111830293128317724, + "backend/tests/test_translate_corrections.py": 14346682408627538824, + "venv/lib/python3.13/site-packages/fastapi/_compat/v2.py": 15078491685917047095, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_datetime.py": 7456744745424297814, + "docs/api/html/search/all_c.js": 9934344868053304351, + "frontend/e2e/e2e-nohealth.mjs": 5921562935931064381, + "docs/api/html/group__test__detect__language__edge__cases.html": 18380807334063203499, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo77.f": 16156304612888138777, + "venv/lib/python3.13/site-packages/dateutil/relativedelta.py": 5903095963079475484, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/__init__.py": 17194870661869873083, + "docs/api/html/group__NextExecutionResponse.html": 8976572706922461958, + "venv/lib/python3.13/site-packages/PIL/GimpPaletteFile.py": 2040459799887417392, + "docs/api/html/group__TerminologyDictionary.html": 430268445994358090, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_stata.py": 3030675968330529905, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_sparc_sun_gcc.h": 2915128971664712180, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/asyncio.py": 11442130378027628520, + "backend/src/api/routes/dashboards/_schemas.py": 436803445837215932, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_user_array.py": 8711697474193296376, + "venv/lib/python3.13/site-packages/pandas/io/formats/css.py": 11288129826285529457, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_calendar.py": 12313641252472031631, + "venv/lib/python3.13/site-packages/idna/intranges.py": 12536174834761591006, + "backend/src/api/routes/maintenance/_schemas.py": 18326740952487508630, + "docs/api/html/group__profile__service.html": 18177058371654387128, + "docs/api/html/group__test__case__insensitive.html": 15150512929501431253, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/reporters.py": 10760889726586439724, + "frontend/src/components/TaskLogViewer.svelte": 15158021998452088463, + "docs/api/html/group__DashboardHealthItem.html": 14118707309412866355, + "venv/lib/python3.13/site-packages/starlette/routing.py": 16063812008141874150, + "docs/api/html/group____dict__to__response.html": 16049941736309608274, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/__init__.py": 8093984834068317399, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.cpython-313-x86_64-linux-gnu.so": 13104135217735363438, + "venv/lib/python3.13/site-packages/PIL/PdfImagePlugin.py": 16082854250442815862, + "backend/src/core/config_manager.py": 16966493197893760632, + "venv/lib/python3.13/site-packages/pandas/tests/libs/__init__.py": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/hooks/applypatch-msg.sample": 12627299623252391515, + "docs/api/html/group__logout__Function.html": 17141287000825844798, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_axis.py": 4097534573847151853, + "backend/src/services/git/_branch.py": 10648641387987189436, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_digest.py": 7988469818593853943, + "venv/lib/python3.13/site-packages/numpy/ma/extras.py": 7559838309481824664, + "docs/api/html/group__MergeSpec.html": 4696541116430893567, + "backend/src/plugins/translate/orchestrator_query.py": 4760681990145471312, + "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiPost.html": 16080559730888993459, + "docs/api/html/group__PermissionsTest.html": 1551075608656490066, + "backend/src/api/routes/assistant/_resolvers.py": 5636628127798453486, + "docs/api/html/group__test__compliance__run__missing__manifest__marks__task__failed.html": 11141720046946032723, + "docs/api/html/globals_g.html": 13803013789474748050, + "docs/api/html/group__DictionaryImport.html": 17459309263375084602, + "venv/lib/python3.13/site-packages/greenlet/greenlet_cpython_compat.hpp": 8396413256279803400, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py": 16571782049241458769, + "docs/api/html/group____latest__approval__for__candidate.html": 8034806470992140564, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_numpy.py": 9663734591667845940, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bus.py": 4260827599660763641, + "frontend/src/components/DashboardGrid.svelte": 3453508702214419189, + "venv/lib/python3.13/site-packages/requests/utils.py": 17045260098091371478, + "docs/api/html/group__createTask__Function.html": 15327860435941970591, + "venv/lib/python3.13/site-packages/jsonschema/exceptions.py": 15651767069704734674, + "docs/api/html/group__SupersetAuthCache__get.html": 5371825726490397800, + "docs/api/html/group__start__check.html": 10263972923109456955, + "backend/src/api/routes/assistant/_tool_health_summary.py": 5500595296925327410, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_emoji_replace.py": 8166203537440976347, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py": 7573043358244876807, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/eoo.py": 5126566925877730303, + "venv/lib/python3.13/site-packages/websockets/asyncio/server.py": 885415484558949506, + "venv/lib/python3.13/site-packages/PIL/Hdf5StubImagePlugin.py": 15654153796225833950, + "docs/api/html/group__handleViewLogs__Function.html": 3848593199895136202, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/__init__.py": 13331622784664204066, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/__init__.py": 15130871412783076140, + "docs/api/html/group__ConnectionContracts.html": 3059252696925019088, + "docs/api/html/group__DashboardHub__hasColumnFilter__Function.html": 16255620732519975073, + "docs/api/html/group__handleCancel__Function.html": 3411761753976161711, + "docs/api/html/group__sanitize__text.html": 15662367841313346736, + "venv/lib/python3.13/site-packages/pandas/_libs/testing.cpython-313-x86_64-linux-gnu.so": 16758333404587340103, + "docs/api/html/group__mask__api__key.html": 16683069110574371100, + "docs/api/html/group__SupersetClientGetDatabaseByUuid.html": 12211338617460141726, + "venv/lib/python3.13/site-packages/starlette/testclient.py": 16088848330558215194, + "docs/api/html/group__DashboardDetailRoutes.html": 8608175949358740288, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py": 16768579082462294745, + "docs/api/html/group__stores__module__Module.html": 5582348367108541718, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_astype.py": 4178231007649608531, + "docs/api/html/group__SupersetDatasetsMixin.html": 1567821220831285583, + "venv/lib/python3.13/site-packages/attrs/validators.py": 10346563361083429998, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayterator.pyi": 15722265241660279277, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pymysql.py": 12923876736367229454, + "docs/api/html/group__SupersetPreviewPipelineTests.html": 7525293242596376133, + "docs/api/html/group__ScheduleAtAGlance.html": 7804111443901031953, + "docs/api/html/group__test__metrics__per__language__breakdown.html": 8039163332388559175, + "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.py": 11621577915097515056, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/RECORD": 16342326945648353387, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_setitem.py": 14920902525192671548, + "frontend/build/_app/immutable/chunks/pE6rV5Bj.js": 6281028968775553497, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/packages.py": 11689856177725665974, + "venv/lib/python3.13/site-packages/pip/_internal/cli/command_context.py": 9426036980437931533, + "venv/lib/python3.13/site-packages/apscheduler/events.py": 10748346950729364899, + "venv/lib/python3.13/site-packages/pyasn1/type/namedtype.py": 1859990775875190116, + "venv/lib/python3.13/site-packages/numpy/random/_philox.cpython-313-x86_64-linux-gnu.so": 9916143171057774705, + "docs/api/html/search/groups_4.js": 16921846554525899660, + "venv/lib/python3.13/site-packages/pillow.libs/libwebpmux-7f11e5ce.so.3.1.2": 13105940093474848953, + "venv/lib/python3.13/site-packages/numpy/_core/defchararray.py": 15550230459945996915, + "venv/lib/python3.13/site-packages/pandas/api/typing/__init__.py": 6438268275857060114, + "docs/api/html/group__process__batch.html": 15294657492230512680, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args.py": 5821040955806257450, + "frontend/src/routes/dashboards/[id]/+page.svelte": 12256445236911281541, + "backend/src/schemas/translate.py": 14818556454502564027, + "docs/api/html/group____extract__id.html": 3759315153888823869, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/events.py": 7576713358952064464, + "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.cpp": 7360124705400993707, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_indexing.py": 16264430536704066847, + "venv/lib/python3.13/site-packages/websockets/legacy/framing.py": 12759022769700230409, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/METADATA": 16498441272591167134, + "docs/api/html/group__CandidateMatchType.html": 12481194623088347299, + "venv/lib/python3.13/site-packages/PIL/ImagePath.py": 18323293175183057101, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_api.py": 1910541672533352625, + "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.pyi": 12144531601504755948, + "frontend/src/lib/api/translate.js": 18140498250896378509, + "venv/lib/python3.13/site-packages/numpy/version.py": 1497063540123613501, + "frontend/src/routes/datasets/StatsBar.svelte": 4241697609925045735, + "venv/lib/python3.13/site-packages/anyio/abc/_resources.py": 8330687082933028623, + "venv/lib/python3.13/site-packages/ecdsa/test_eddsa.py": 1785854649532849946, + "docs/api/html/group__LanguageDetectService.html": 5256473727119538247, + "docs/api/html/group__src__api.html": 13647182032403294242, + "venv/lib/python3.13/site-packages/pandas/util/_tester.py": 1033951890304257539, + "venv/lib/python3.13/site-packages/pygments/formatter.py": 11156733836937309465, + "docs/api/html/group__test__override__root__path.html": 18363961162670447666, + "venv/lib/python3.13/site-packages/_pytest/python.py": 1829073958095716309, + "venv/lib/python3.13/site-packages/ecdsa/rfc6979.py": 11564966030899162026, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_free.f90": 10939657740734987454, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polynomial.py": 777287409151500309, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append_common.py": 2377351120083195530, + "docs/api/html/group__test__expired__event__auto__ends.html": 11366519398118405696, + "backend/git_repos/remote/test-repo.git/objects/4e/227f4d8289ea657ae813b1460f979ae4b676b3": 5659284628452785331, + "venv/lib/python3.13/site-packages/requests/api.py": 12339139862411341381, + "venv/lib/python3.13/site-packages/numpy/_typing/_nested_sequence.py": 7827259347937485408, + "docs/api/html/group__TranslationPreview.html": 2562373106089262703, + "docs/api/html/group__tasks__engine.html": 7169537698069676064, + "docs/api/html/group__BatchApproveSemanticItemRequest.html": 8441557849246446003, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/py.typed": 15130871412783076140, + "backend/src/core/utils/superset_context_extractor/_base.py": 6349561308410782726, + "venv/lib/python3.13/site-packages/jose/jwe.py": 3325660353225624025, + "backend/src/api/routes/validation.py": 17157318051370565853, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_inspect.py": 10573558345297891848, + "backend/tests/test_resource_hubs.py": 16183993941366005066, + "venv/lib/python3.13/site-packages/pycparser/ply/__init__.py": 5040371336745852622, + "docs/api/html/group__start.html": 18303777950604936330, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_diff.py": 7420674239386436885, + "docs/api/html/group__test__maintenance__service.html": 4028053446391236615, + "docs/api/html/group____get__preference__row.html": 10336167497316255170, + "docs/api/html/group__SupersetClientFetchTotalObjectCount.html": 6342114507024365275, + "venv/lib/python3.13/site-packages/pygments/lexers/webassembly.py": 17811970490091406867, + "docs/api/html/group__PreparePreviewCommand.html": 3982816096514415073, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/METADATA": 732041908491732594, + "docs/api/html/search/close.svg": 6737892306857409458, + "docs/api/html/group__resetDataTimeout__Function.html": 1556809234079451125, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-2.csv": 4052796393463357888, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_crosstab.py": 6631711416347033281, + "docs/api/html/group__DashboardHub__updateColumnFilterSearch__Function.html": 10268710448771778339, + "docs/api/html/group__LaunchDatasetResult.html": 13328722234223010453, + "venv/lib/python3.13/site-packages/PIL/MspImagePlugin.py": 3034490726971829439, + "docs/api/html/group__SidebarTest__Module.html": 17956644577281325892, + "venv/lib/python3.13/site-packages/numpy/core/numerictypes.py": 5776941405348611027, + "venv/lib/python3.13/site-packages/git/compat.py": 16843345645628515492, + "docs/api/html/group__MaintenanceEvent.html": 7096871059783087964, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/properties.py": 5799883762127872919, + "venv/lib/python3.13/site-packages/PIL/PpmImagePlugin.py": 7107324729714470853, + "docs/api/html/group__test__migration__dry__run__service__builds__diff__and__risk.html": 8110338995092448857, + "venv/lib/python3.13/site-packages/pycparser/_ast_gen.py": 5464347629908906775, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/METADATA": 3179724236887811680, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/contains.py": 3098087827310439993, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_print.py": 598821365691498702, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_where.py": 3272402211717453881, + "venv/lib/python3.13/site-packages/sqlalchemy/event/legacy.py": 13465911776953636949, + "venv/lib/python3.13/site-packages/httpcore/_backends/base.py": 5491727479625093183, + "docs/api/html/group__ReportModels.html": 12439114738308381242, + "docs/api/html/group__test__enable__disable__schedule.html": 10584328518682693637, + "docs/api/html/group__openGit__Function.html": 5275859006159727877, + "venv/lib/python3.13/site-packages/starlette/exceptions.py": 15978811612997200737, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/ansi.py": 948249091929588017, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multithreading.py": 12547048606098186681, + "docs/api/html/group__AsyncAPIClient__aclose.html": 207928245913411569, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_api.py": 13739775047493489785, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_searchsorted.py": 11208047646304753715, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_iterrows.py": 3791886201566670087, + "backend/src/api/routes/assistant/_dataset_review.py": 4389080462745572364, + "venv/lib/python3.13/site-packages/pip/_internal/utils/virtualenv.py": 14416705841810221305, + "venv/lib/python3.13/site-packages/numpy/lib/__init__.pyi": 1537495515469182226, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/__init__.py": 16211693040174992594, + "venv/lib/python3.13/site-packages/numpy/_core/_methods.pyi": 9155045948490225716, + "venv/lib/python3.13/site-packages/attr/setters.pyi": 6670509359139243236, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/WHEEL": 2823347510103459191, + "venv/lib/python3.13/site-packages/pip/_internal/operations/freeze.py": 3469845570614985559, + "backend/src/api/routes/git/_environment_routes.py": 5687847761729712859, + "backend/src/services/clean_release/__tests__/test_policy_engine.py": 7872293927249163831, + "docs/api/html/group__test__no__schema__qualified__tables.html": 4881082392133398377, + "venv/lib/python3.13/site-packages/_pytest/freeze_support.py": 494952714451267295, + "venv/lib/python3.13/site-packages/pygments/lexers/_php_builtins.py": 2344562378679281201, + "frontend/src/components/git/GitOperationsPanel.svelte": 5140779139910068800, + "venv/lib/python3.13/site-packages/jaraco/context/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic/json_schema.py": 15678278224696353978, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_ccalendar.py": 8819357623337533489, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/licenses/LICENSE": 11199044866758471950, + "docs/api/html/group__test__tui__f5__dispatches__run__action.html": 799267684182155396, + "backend/src/plugins/translate/__tests__/test_dictionary_utils.py": 2896829624338027530, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_reindex.py": 15680924058960901067, + "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.cpython-313-x86_64-linux-gnu.so": 16963890771704942465, + "docs/api/html/group__test__enable__belief__state__flag.html": 9288819795406438186, + "venv/lib/python3.13/site-packages/pygments/lexers/_mapping.py": 12987629396101138833, + "venv/lib/python3.13/site-packages/apscheduler/executors/gevent.py": 17122637996105344344, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_missing.py": 6224750552277937853, + "venv/lib/python3.13/site-packages/pandas/io/formats/html.py": 11849941990375030601, + "venv/lib/python3.13/site-packages/pillow.libs/libjpeg-32d42e18.so.62.4.0": 11717944412021329207, + "venv/lib/python3.13/site-packages/cryptography/utils.py": 18088508841978595788, + "venv/lib/python3.13/site-packages/uvicorn/config.py": 17317823506849880916, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_type_check.py": 3935091606923106753, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.pyi": 16164841755339571545, + "docs/api/html/group__ReportsPageTest__Module.html": 7688339467374188309, + "docs/api/html/group__test__add__validation__job.html": 17097612352066161020, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 2991745295485163924, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libldap-2974a1ba.so.2.0.200": 13590569080612602445, + "venv/lib/python3.13/site-packages/pandas/core/indexes/timedeltas.py": 13653393430363556843, + "docs/api/html/group__SupersetClientInit.html": 12194497519496768637, + "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.py": 1590647503660777156, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_construction.py": 10619501873883861745, + "backend/src/core/superset_client/_layout_utils.py": 13377525832228646249, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/RECORD": 1689342214025776630, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_drop_duplicates.py": 12601598844977346213, + "venv/lib/python3.13/site-packages/cffi/_imp_emulation.py": 17246725564704952212, + "frontend/build/_app/immutable/chunks/BdxW4FPV.js": 5155292967366452793, + "docs/api/html/group__extract__dataset__id__from__form__data.html": 6984612851511739917, + "venv/lib/python3.13/site-packages/pandas/api/interchange/__init__.py": 181911269762882069, + "backend/src/dependencies.py": 11284407227879550824, + "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp_impl.cpython-313-x86_64-linux-gnu.so": 11767771662042404251, + "docs/api/html/search/pages_0.js": 9801887205019013612, + "venv/lib/python3.13/site-packages/numpy/random/_sfc64.cpython-313-x86_64-linux-gnu.so": 17186695607612591922, + "docs/api/html/group__TrustLevel.html": 10252007574843682167, + "venv/lib/python3.13/site-packages/pip/_internal/network/auth.py": 6159841209618753296, + "venv/lib/python3.13/site-packages/numpy/fft/__init__.py": 9343449448737026658, + "docs/api/html/group__fetchPlugins__Function.html": 3672886601071817310, + "docs/api/html/globals_func_q.html": 11705790463608300649, + "venv/lib/python3.13/site-packages/typing_extensions.py": 8402965285733247527, + "backend/src/plugins/maintenance_banner.py": 13137032570032257498, + "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.py": 4984721349618127021, + "venv/lib/python3.13/site-packages/uvicorn/protocols/__init__.py": 15130871412783076140, + "venv/bin/activate.csh": 14513616043256836238, + "docs/api/html/group__TaskLogRecord.html": 14921887044252207523, + "venv/lib/python3.13/site-packages/pygments/lexers/c_cpp.py": 1211842877043321095, + "docs/api/html/group__frontend__src__services__adminService__js____updateRole.html": 1298256109882565356, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/errors.py": 10750605030539582738, + "venv/lib/python3.13/site-packages/httpcore/_exceptions.py": 6716590711697088732, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/RECORD": 9974324769263969827, + "venv/lib/python3.13/site-packages/ecdsa/test_pyecdsa.py": 14542286382742044648, + "venv/lib/python3.13/site-packages/pydantic/_internal/_model_construction.py": 694471028676746110, + "docs/api/html/group____repo__with__seed__data.html": 17628026577432977931, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.cpython-313-x86_64-linux-gnu.so": 15960606468596245469, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_partial.py": 15129217814262096441, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/identity.py": 326059526167697655, + "backend/src/models/config.py": 17311376253737985673, + "backend/alembic/versions/86c7b1d6a710_add_is_admin_column_to_roles_table.py": 13324818245013782957, + "docs/api/html/group__DashboardItem.html": 6170980058100567138, + "venv/lib/python3.13/site-packages/pandas/core/indexes/api.py": 13750417165301467381, + "docs/api/html/group__FeedbackResponse.html": 9253665785027046545, + "docs/api/html/group__save.html": 1333375505490554916, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_slp_switch.py": 16490710972475154509, + "venv/lib/python3.13/site-packages/keyring/cli.py": 10996746519742056101, + "venv/lib/python3.13/site-packages/pandas/io/json/_table_schema.py": 3103776773020058805, + "scripts/generate_financial_arrears_data.py": 15989529185799374517, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_types.py": 651959450717569029, + "venv/lib/python3.13/site-packages/rsa/parallel.py": 4968566722541778371, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_loadtxt.py": 17562784940619942447, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/__init__.py": 15130871412783076140, + "docs/api/html/group____find__dashboard__id__by__slug__async.html": 17708878383316867946, + "venv/lib/python3.13/site-packages/pandas/core/strings/object_array.py": 17631047188839635539, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/test_list.py": 4538625980063375611, + "venv/lib/python3.13/site-packages/pygments/sphinxext.py": 2768161754523156130, + "backend/src/api/routes/assistant/_tool_create_branch.py": 182250324409096785, + "venv/lib/python3.13/site-packages/urllib3/util/connection.py": 15511685694589961094, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ma.py": 15912582628112164509, + "venv/lib/python3.13/site-packages/pandas/core/window/online.py": 13551140636252302201, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/mercurial.py": 335201628064585213, + "docs/api/html/group__delete__dictionary__entry.html": 3989967080669519654, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_convert_dtypes.py": 13612212066710266457, + "docs/api/html/group__MaintenanceModels.html": 14224150703489813843, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_unsupported.py": 9115440166074553860, + "docs/api/html/group__DetectedIssue.html": 49569046639972610, + "backend/src/services/superset_lookup_service.py": 131886522963644068, + "docs/api/html/group__serialize__preview__record.html": 17364001740956652340, + "docs/api/html/plusd.svg": 5924202389722020174, + "docs/api/html/group__test__delete__run__success.html": 1498123514744381906, + "docs/api/html/group____get__or__create__preference__row.html": 5428234839987069116, + "docs/api/html/group__BuildManifestRequest.html": 4763470685395243475, + "backend/src/services/clean_release/report_builder.py": 14118825155100404762, + "docs/api/html/group__TestLayoutUtils.html": 3035905040939536258, + "docs/api/html/group__CleanReleaseModels.html": 12685389086616523632, + "docs/api/html/group__ValidationTaskConfig.html": 9838515327973111291, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_index_equal.py": 10617870815989304510, + "docs/api/html/group__test__delete__task__success.html": 6168518344728381187, + "venv/lib/python3.13/site-packages/keyring/backend_complete.bash": 16589214602970214174, + "venv/lib/python3.13/site-packages/pygments/styles/onedark.py": 17580335614588531275, + "frontend/src/services/git-utils.js": 12982149055119314642, + "docs/api/html/search/all_3.js": 16759903393811042899, + "docs/api/html/group__test__git__service__ensure__gitflow__branches__creates__and__pushes__missing__defaults.html": 831567849032163336, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/controller.py": 5450931608244830335, + "docs/api/html/group__AssistantDispatch.html": 16287296406438246956, + "venv/lib/python3.13/site-packages/greenlet/TExceptionState.cpp": 17617354576371846109, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/_json.py": 6718137926109998531, + "backend/src/services/clean_release/__tests__/test_manifest_builder.py": 4923613189073397440, + "venv/lib/python3.13/site-packages/numpy/tests/test_matlib.py": 14395481530627870882, + "venv/lib/python3.13/site-packages/pygments/styles/coffee.py": 17584414468786822371, + "docs/api/html/group__LinkedDashboard.html": 16245670045855947237, + "docs/api/html/group__test__extract__native__filters__from__key__single__filter.html": 17303628301397310552, + "venv/lib/python3.13/site-packages/pandas/core/reshape/encoding.py": 12702053687535473061, + "backend/src/plugins/translate/__tests__/test_lang_detect.py": 2328877106785068272, + "venv/lib/python3.13/site-packages/typing_inspection/introspection.py": 13916695705179237047, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_multiindex.py": 13723677252432338937, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray_misc.pyi": 16219573290116396099, + "venv/lib/python3.13/site-packages/cryptography/hazmat/__init__.py": 6534110674636458866, + "docs/api/html/group__test__pull__changes__applies__profile__identity__before__pull.html": 16120757619270766931, + "docs/api/html/group__test__tui__f5__run__smoke__reports__blocked__state.html": 5494023614389428629, + "docs/api/html/group____normalize__owner__display__token.html": 12722166268029895042, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sin.csv": 1443087417808027268, + "venv/lib/python3.13/site-packages/pygments/lexers/_sql_builtins.py": 5906655440517434734, + "docs/api/html/group__test__limited__user__cannot__launch__restricted__operation.html": 3773775103018800523, + "docs/api/html/group__test__root__path__is__string.html": 16492802671590347100, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npy": 9694707264403109829, + "venv/lib/python3.13/site-packages/pygments/lexers/html.py": 17288963642862550116, + "venv/lib/python3.13/site-packages/pydantic/v1/annotated_types.py": 12925825196690299531, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/tools.py": 6437315459200419827, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_excel.py": 5467043641314706635, + "frontend/src/lib/stores/datasetReviewSession.js": 12759266804307584180, + "backend/alembic/versions/8dd0a93af539_drop_deprecated_source_language_column_.py": 8346802799348229086, + "docs/api/html/group__run.html": 11476921346246444221, + "docs/api/html/group__AssistantConfirmationRecord.html": 3562498999897451626, + "venv/lib/python3.13/site-packages/websockets/sync/utils.py": 15135020989207191168, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/blocking.py": 3483388533957772681, + "venv/lib/python3.13/site-packages/keyring/http.py": 1408024724330078009, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/jwt_bearer.py": 6973859422068490274, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/control.py": 2034518223411945438, + "docs/api/html/group__test__sidebar__initial__state__Function.html": 1960834257863152813, + "docs/api/html/group__handleDeleteReport__Function.html": 6406518708534911213, + "venv/lib/python3.13/site-packages/passlib/crypto/digest.py": 15890463288512025688, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_round.py": 7912161077874611432, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_multi.py": 4612816468258071810, + "venv/lib/python3.13/site-packages/pygments/styles/arduino.py": 4578385147314541233, + "frontend/src/lib/components/layout/sidebarNavigation.js": 14306348671588504207, + "docs/api/html/search/functions_7.js": 7432376903275069025, + "backend/git_repos/remote/test-repo.git/objects/4f/4e0958b3a42eff67a2b8cfd084d05c43e54716": 6040513978194442637, + "venv/lib/python3.13/site-packages/websockets/legacy/exceptions.py": 16025779645869134942, + "docs/api/html/group__update__task.html": 2054851862802669708, + "venv/lib/python3.13/site-packages/pandas/compat/numpy/function.py": 18405870240333383757, + "backend/src/services/git_service.py": 12429116089116885657, + "docs/api/html/group__MigrationPackage.html": 671195743151997854, + "docs/api/html/group__get__logs.html": 15112919543989403638, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_csky_gcc.h": 12929785267195988209, + "docs/api/html/group__test__future__end__time__skipped.html": 4190142045276084379, + "venv/lib/python3.13/site-packages/_pytest/_version.py": 4890545974654141536, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_protocols.py": 5019018645804313814, + "venv/lib/python3.13/site-packages/passlib/ifc.py": 14479133654556657428, + "docs/api/html/group__test__empty__model__returns__false.html": 15426733626551187888, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_fillna.py": 7349777469648334282, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexers.py": 6773751569163287047, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_str.py": 149688376805129532, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_numeric.py": 18037815278545274402, + "frontend/src/lib/components/reports/__tests__/reports_filter_performance.test.js": 6358696236009732713, + "docs/api/html/group____load__config.html": 9007854862967223337, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/zip-safe": 15240312484046875203, + "docs/api/html/group__LLMProviderService.html": 12709095326863843331, + "venv/lib/python3.13/site-packages/PIL/_webp.cpython-313-x86_64-linux-gnu.so": 11070012119632935103, + "venv/lib/python3.13/site-packages/pygments/lexers/maple.py": 7402467573829618534, + "docs/api/html/search/groups_1.js": 5807190646834111663, + "docs/api/html/group__subscribe__logs.html": 16463697240897933473, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/__init__.py": 9083396324667433335, + "venv/lib/python3.13/site-packages/pydantic_settings/main.py": 17272314000918984790, + "venv/lib/python3.13/site-packages/rsa/asn1.py": 3861968161559322784, + "docs/api/html/group__CleanReleaseCliScript.html": 9859420981765754979, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawerForTask.html": 5669096331612000811, + "docs/api/html/group__cot__log__function.html": 609040572561257151, + "venv/lib/python3.13/site-packages/pip/_internal/utils/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/array_utils.pyi": 14646405559193539455, + "venv/lib/python3.13/site-packages/numpy/_typing/_dtype_like.py": 1598511035344858524, + "venv/lib/python3.13/site-packages/pandas/core/col.py": 4221271461723119525, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reset_index.py": 14490875921264161445, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_xs.py": 1897144171324167738, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_reductions.py": 3227705565578016389, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_join.py": 1364886077926604427, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/__init__.py": 7877061921210840955, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/__init__.py": 9106887608906281686, + "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.py": 5570375156851166818, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__run__mapping.html": 4039233635360893448, + "venv/lib/python3.13/site-packages/dotenv/__main__.py": 16883579879172186355, + "docs/api/html/group__setup__method.html": 17742160113140228512, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/x963kdf.py": 14995426099932276090, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/warnings_and_errors.pyi": 9019577563960071498, + "venv/lib/python3.13/site-packages/pandas/core/ops/__init__.py": 4049432353810047365, + "venv/lib/python3.13/site-packages/pygments/styles/monokai.py": 12381683616720952636, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_formats.py": 13500155167624433622, + "venv/bin/keyring": 1488968652276547659, + "docs/api/html/group__test__rebuild__active__banner.html": 5240773112878811635, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_palettes.py": 13495896173068490516, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_shift.py": 9952689832827833247, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_arrow_compat.py": 17818294260710640443, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/__init__.py": 63497920264089252, + "frontend/src/types/dashboard.ts": 9718666955722367566, + "venv/lib/python3.13/site-packages/PIL/XVThumbImagePlugin.py": 7472134175570205454, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/gitdb/test/__init__.py": 18393574323937353933, + "venv/lib/python3.13/site-packages/pydantic/v1/decorator.py": 6124748023704700724, + "venv/lib/python3.13/site-packages/requests/sessions.py": 6936640856092320928, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_clip.py": 8087139801274517318, + "docs/api/html/group__test__duplicate__job.html": 16156801753517777022, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_ios.h": 6751259472945638998, + "docs/api/html/group__test__api__update__job__not__found.html": 17098826918877275745, + "docs/api/html/globals_j.html": 16316103223566935047, + "venv/lib/python3.13/site-packages/pyasn1/__init__.py": 8089201217593738032, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_align.py": 6050248183327934780, + "venv/lib/python3.13/site-packages/pandas/_version_meson.py": 4875297137037630519, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/entities.py": 8001287806688585081, + "venv/lib/python3.13/site-packages/numpy/lib/scimath.py": 7616377937400918724, + "venv/lib/python3.13/site-packages/fastapi/applications.py": 4975013401500183420, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/decoder.py": 12653470212935324322, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_masked.py": 5380551301916549925, + "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.py": 15558318749939680074, + "venv/lib/python3.13/site-packages/numpy/testing/overrides.pyi": 17408923209056015964, + "backend/src/plugins/llm_analysis/__tests__/test_client_headers.py": 9559902355749971525, + "docs/api/html/group____FakeTask.html": 16676856916522433598, + "frontend/playwright-report/data/288dba8d36913a1ca2ebbe6ef75a4997ea72bb65.png": 4602930255871194331, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/RECORD": 16874983218652404911, + "frontend/src/lib/stores/__tests__/setupTests.js": 16228706107748081093, + "docs/api/html/group____sanitize__optional__identity__value.html": 6453024202123093607, + "docs/api/html/group____build__risks.html": 10011809177370998034, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_block_internals.py": 17935662848672401507, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/stride_tricks.pyi": 5844806555773292985, + "frontend/build/_app/immutable/entry/app.hvuIRbkr.js": 7494248522021054463, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_spinners.py": 15873416546130487469, + "venv/lib/python3.13/site-packages/numpy/_pytesttester.pyi": 5750506937526308405, + "venv/lib/python3.13/site-packages/urllib3/util/timeout.py": 17922362393077965047, + "venv/lib/python3.13/site-packages/numpy/_configtool.pyi": 9670892208858157218, + "docs/api/html/group__BulkReplaceModalTests.html": 16436012889032153667, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/integration.py": 13610682193839598457, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/certs.py": 13366788049827102313, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_quarter.py": 6795026338946382805, + "venv/lib/python3.13/site-packages/pyasn1/compat/integer.py": 7365270971777550573, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/foo_deps.f90": 14350239197565389508, + "docs/api/html/group__AsyncSupersetClientClose.html": 17574786361007292921, + "frontend/src/lib/components/assistant/AssistantClarificationCard.svelte": 4264349064103133826, + "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.py": 13115032674230220791, + "docs/api/html/group__fetch__paginated__data.html": 2029155159485988556, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_read.py": 8825002468745406134, + "venv/lib/python3.13/site-packages/pandas/io/formats/format.py": 3364941338174434661, + "docs/api/html/group__test__dataset__review__scoped__message__uses__masked__filter__context.html": 17334328430934635110, + "frontend/src/components/auth/ProtectedRoute.svelte": 9152870722988000278, + "backend/src/plugins/storage/__init__.py": 5046554941488079204, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reorder_levels.py": 10996225584998355824, + "docs/api/html/group__explore.html": 2270854509477383382, + "docs/api/html/group__SupersetClientGetDashboardsSummary.html": 6037771020479068589, + "venv/lib/python3.13/site-packages/pandas/core/reshape/melt.py": 14109947554124551986, + "frontend/src/lib/components/ui/DatasetSearchCombobox.svelte": 14283363077342392531, + "docs/api/html/group__getReportDetail__Function.html": 15901398277426320441, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/WHEEL": 14550174241288068152, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/METADATA": 11759004913807981650, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_export_format.py": 17862222951776199694, + "venv/lib/python3.13/site-packages/jsonschema/__init__.py": 6925762200139836824, + "venv/lib/python3.13/site-packages/keyring/util/platform_.py": 16978095707950463638, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_loc.py": 2127584756555503873, + "docs/api/Doxyfile": 3726528055138990005, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_.py": 644667973700652263, + "venv/lib/python3.13/site-packages/h11/_state.py": 11398755254809751139, + "venv/lib/python3.13/site-packages/numpy/lib/format.pyi": 11752225430594536662, + "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending_distributions.py": 209769687436320665, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/_vbscript_builtins.py": 9882134558469491349, + "docs/api/html/group__read__root.html": 3120255621105556309, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/__init__.py": 15130871412783076140, + "docs/api/html/group__SupersetDashboardsFiltersMixin.html": 17578063671731484965, + "venv/lib/python3.13/site-packages/greenlet/TUserGreenlet.cpp": 16258622236210473101, + "docs/api/html/group__set__schedule.html": 8284012278188829172, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isetitem.py": 14895552405828068177, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/__init__.py": 15130871412783076140, + "docs/api/html/group__DICT__TOKENS__MAX.html": 5949599154091293863, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5-fcafa220.so.3.3": 5620628698712971642, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py": 17786759398959482432, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.f90": 1249061697041863692, + "frontend/src/routes/settings/SystemSettings.svelte": 17084926955388049976, + "venv/lib/python3.13/site-packages/pluggy/_result.py": 16790448391766505769, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py": 15130871412783076140, + "docs/api/html/group__deleteRole__Function.html": 10988783686438944545, + "docs/api/html/group__ClarificationEngine.html": 9404742488372089320, + "docs/api/html/group__CleanPolicyEngine.html": 5866910644172522724, + "venv/lib/python3.13/site-packages/secretstorage/__init__.py": 4535964275649588672, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pluggy/__init__.py": 8269188363563356631, + "venv/lib/python3.13/site-packages/iniconfig/exceptions.py": 16501270224069836264, + "venv/lib/python3.13/site-packages/pandas/io/spss.py": 864838144958947906, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/provision.py": 8707620698060818654, + "docs/api/html/group__get__detector.html": 10160834986755311488, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/aead.pyi": 13061638961405460334, + "docs/api/html/group__update__session.html": 14739091990972920218, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_fixed.f90": 8079594129222364031, + "venv/lib/python3.13/site-packages/starlette/middleware/authentication.py": 15211131264936212938, + "frontend/src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js": 12582020574293031526, + "venv/lib/python3.13/site-packages/pydantic/v1/version.py": 2758971830527526972, + "docs/api/html/group__test__build__namespaced__id__prevents__cross__mode__collisions.html": 3515946137513241293, + "docs/api/html/group__preview__prompt__helpers.html": 17763275894621267369, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/METADATA": 9586902338916246692, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_partial.py": 3151286293440416036, + "docs/api/html/group__read__dashboard__from__disk.html": 6267975408342998536, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/requirements.py": 2362917888844255805, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_arithmetic.py": 976970179757115407, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/unrolled.py": 12651425412640834530, + "docs/api/html/group__ApproveMappingRequest.html": 3410582796828079029, + "venv/lib/python3.13/site-packages/fastapi/security/__init__.py": 1345944646061334046, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/REQUESTED": 15130871412783076140, + "docs/api/html/group__BulkFindReplaceService.html": 13762621363378124184, + "venv/lib/python3.13/site-packages/pygments/lexers/factor.py": 15788613154609197002, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_apply.py": 11248068903302454630, + "venv/lib/python3.13/site-packages/pip/_internal/locations/_sysconfig.py": 4773855595767526182, + "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key__slug__resolution__fails.html": 5436713915568993137, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_clip.py": 13498704233745189154, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/warnings.py": 2552021607611112605, + "venv/lib/python3.13/site-packages/anyio/streams/text.py": 12507591237176920223, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/base.py": 5294446834530589760, + "frontend/playwright.config.js": 802586619504012017, + "docs/api/html/group____normalize__actor__alias__token.html": 5763510838664670977, + "venv/lib/python3.13/site-packages/pydantic/decorator.py": 6984214881365537811, + "venv/lib/python3.13/site-packages/pygments/styles/dracula.py": 12379322008538174078, + "venv/lib/python3.13/site-packages/numpy/random/_pcg64.pyi": 14785496840737986355, + "docs/api/html/group__sanitize__imported__filter__for__assistant.html": 11096067160073736807, + "docs/api/html/group__test__detect__language__basic.html": 13601975669344229958, + "venv/lib/python3.13/site-packages/numpy/ma/testutils.pyi": 3899348061346041413, + "docs/api/html/group__frontend__src__services__git__utils__js____getSelectedConfig.html": 1957773391502963518, + "docs/api/html/group__test__prepare__candidate__blocks__external__source.html": 6336322874440712308, + "docs/api/html/group__ScheduleSchema.html": 6081468916671081411, + "venv/lib/python3.13/site-packages/jeepney/bus_messages.py": 5871341372541525842, + "venv/lib/python3.13/site-packages/pip/_internal/locations/base.py": 7966628150470073365, + "docs/api/html/group__AuthSecurityModule.html": 3598351476195431872, + "docs/api/html/group__SessionCollaborator.html": 5083895824126893676, + "venv/lib/python3.13/site-packages/pip/_internal/commands/wheel.py": 16397273591245583356, + "docs/api/html/group__test__start__too__many__tables.html": 8520186301114425960, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine_first.py": 11425059090050867171, + "docs/api/html/group__ApprovalService.html": 5375853220886231078, + "docs/api/html/group__test__cli__manifest__build__scaffold.html": 12976660891449415796, + "venv/lib/python3.13/site-packages/pandas/tests/series/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge.py": 11366935134260232329, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_libsparse.py": 1859997684694949029, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/create.py": 16693261531832306870, + "docs/api/html/group__updateUploadCategory__Function.html": 17493381273334242237, + "docs/api/html/graph_legend.html": 11133044628946024695, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_trio.py": 7459901627473962540, + "venv/lib/python3.13/site-packages/attrs/__init__.pyi": 14353885498883901855, + "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.py": 548928042543500365, + "docs/api/html/group__DashboardHub__getLlmSummaryLabel__Function.html": 17960034083462652821, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils.py": 10362977406431569031, + "docs/api/html/group__SecurityBadgeService.html": 10559304755960949908, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_arithmetic.py": 13320788210970132239, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_constructors.py": 3440572832185940666, + "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.pyi": 8879196542984811747, + "docs/api/html/group__test__delete__git__config__removes__record.html": 6921631931687420246, + "venv/lib/python3.13/site-packages/jsonschema/_typing.py": 13152346021901861822, + "docs/api/html/group__test__permission__denied__all__actions.html": 9271173936930380116, + "venv/lib/python3.13/site-packages/pandas/core/resample.py": 13814298871925341617, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/__init__.py": 9820770319106921495, + "venv/lib/python3.13/site-packages/httpcore/_async/interfaces.py": 18250473733460571816, + "venv/lib/python3.13/site-packages/pygments/lexers/_qlik_builtins.py": 1728058904137960444, + "venv/bin/pyrsa-verify": 6762739642629904442, + "venv/lib/python3.13/site-packages/uvicorn/_types.py": 3339389597716220585, + "venv/lib/python3.13/site-packages/fastapi/middleware/trustedhost.py": 4285622102799027823, + "docs/api/html/group__test__belief__scope__not__visible__at__info.html": 7650312905986638988, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getRecommendedActionLabel.html": 4071530290797057104, + "venv/lib/python3.13/site-packages/PIL/FliImagePlugin.py": 13364963053197422923, + "docs/api/html/group__test__patch__profile__preferences__validation__error.html": 8272861585949821764, + "docs/api/html/group__AssistantRoutes.html": 11177343112252329246, + "venv/lib/python3.13/site-packages/authlib/jose/errors.py": 15231759002942436632, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_item_selection.py": 1027989019709029615, + "frontend/src/lib/components/reports/__tests__/reports_page.integration.test.js": 966219842574639412, + "frontend/build/_app/immutable/nodes/10.Dkya0-H0.js": 11170179190374974039, + "backend/src/models/maintenance.py": 6429396762236968499, + "venv/lib/python3.13/site-packages/jeepney/io/threading.py": 525625939662612746, + "docs/api/html/group__tasks__Data.html": 8580763785347797438, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_melt.py": 15663807247682146566, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/types.py": 12298292195929126306, + "docs/api/html/group__list__api__keys.html": 13338086264861974569, + "backend/src/plugins/translate/_llm_call.py": 8396628902584705345, + "docs/api/html/group__test__json__load__if__needed__json__string.html": 13679424805613281118, + "venv/lib/python3.13/site-packages/_pytest/python_api.py": 12953851690889336092, + "venv/lib/python3.13/site-packages/pandas/_libs/internals.pyi": 11662356720550765508, + "venv/lib/python3.13/site-packages/numpy/lib/_version.pyi": 10804755840730584157, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/ops.py": 13451323660386712332, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_nonkeyword_arguments.py": 11676809974120007408, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/METADATA": 2810323639884879396, + "venv/lib/python3.13/site-packages/pandas/core/indexing.py": 15507869313676722071, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_3kcompat.h": 5377305123299457337, + "venv/lib/python3.13/site-packages/pygments/lexers/mojo.py": 8616283481592061285, + "frontend/src/routes/datasets/review/ReviewWorkspaceHeader.svelte": 15302107925819930527, + "frontend/build/_app/immutable/chunks/DiTw1IB7.js": 17504270074466006486, + "venv/lib/python3.13/site-packages/attr/_config.py": 1864824728504480204, + "venv/lib/python3.13/site-packages/fastapi/temp_pydantic_v1_params.py": 9436036880518047720, + "docs/api/html/group____append__history.html": 2245659271654816283, + "backend/src/services/__tests__/test_llm_prompt_templates.py": 12364821019604393930, + "venv/lib/python3.13/site-packages/charset_normalizer/cli/__init__.py": 9702005609966176016, + "backend/src/plugins/translate/orchestrator.py": 17822856047409430170, + "venv/lib/python3.13/site-packages/pygments/lexers/typst.py": 8883289726601667104, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.pyi": 3573629536236108391, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_info.py": 10615285239043745712, + "docs/api/html/group__test__storage__model__compiles.html": 12703678636634941542, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/url.py": 9692934100602135106, + "venv/lib/python3.13/site-packages/rsa/pkcs1_v2.py": 3533398238491175229, + "docs/api/html/group__TestMigrationRoutes.html": 12902236514120491768, + "docs/api/html/group____clarification__text__for__intent.html": 15580443210255744848, + "docs/api/html/group__test__get__dashboards__not__found.html": 6573102849582362504, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_mrecords.py": 15952147778503828707, + "venv/lib/python3.13/site-packages/dotenv/__init__.py": 15464030923609356017, + "venv/lib/python3.13/site-packages/numpy/_core/_rational_tests.cpython-313-x86_64-linux-gnu.so": 13475521165331256711, + "venv/lib/python3.13/site-packages/pandas/core/_numba/executor.py": 2205130934861422769, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarrayobject.h": 7320510589635636690, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_apply.py": 7511915990531229598, + "venv/lib/python3.13/site-packages/authlib/oauth1/__init__.py": 3692257288065292607, + "docs/api/html/group__frontend__src__services__adminService__js____getUsers.html": 5532590091702017666, + "venv/lib/python3.13/site-packages/urllib3/response.py": 1987354357306756790, + "venv/lib/python3.13/site-packages/greenlet/tests/test_tracing.py": 10227316489696807112, + "docs/api/html/group__DashboardProfileOverrideIntegrationTest__Module.html": 4086296085552401797, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_explode.py": 16195468192763502449, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/LICENSE": 11167201188673981085, + "backend/src/api/routes/validation/__tests__/test_validation_api.py": 17108559501472409127, + "venv/lib/python3.13/site-packages/pandas/_config/__init__.py": 3577026529387563519, + "frontend/src/lib/api/validation.js": 10650815174196870103, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.cpython-313-x86_64-linux-gnu.so": 13209491675968970365, + "docs/api/html/search/all_e.js": 4022996882705418530, + "frontend/build/_app/immutable/nodes/17.DNk6icE1.js": 10207358215043305384, + "venv/lib/python3.13/site-packages/apscheduler/__init__.py": 8652514118183227412, + "docs/api/html/group____make__user.html": 10705276908709984170, + "venv/lib/python3.13/site-packages/pandas/util/version/__init__.py": 3844072588696074141, + "docs/api/html/folderopen.svg": 11265655722809871940, + "docs/api/html/docd.svg": 5892816938133793015, + "venv/lib/python3.13/site-packages/numpy/_core/function_base.pyi": 8343632640078378312, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_base.py": 8678387511873412922, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.pyi": 14543653817631274394, + "backend/src/models/__tests__/test_models.py": 7136192055672203219, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_shape_manipulation.pyi": 3533739482029983817, + "docs/api/html/group__PreparationService.html": 8547646096904159114, + "docs/api/html/group__test__calculate__schedule__even__distribution.html": 7169536723006571370, + "docs/api/html/group__TranslateApi.html": 7635708107203092858, + "docs/api/html/group__MaintenanceRoutesPackage.html": 8654226804027186583, + "docs/api/html/group__TranslateJobRoutesModule.html": 6272089785836240720, + "venv/lib/python3.13/site-packages/websockets/__main__.py": 11948055520555854616, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/converter.py": 9041278869295701566, + "venv/lib/python3.13/site-packages/numpy/random/_pickle.pyi": 16081120364372391381, + "venv/lib/python3.13/site-packages/dateutil/rrule.py": 398469817784230512, + "backend/logs/app.log.2": 1282701464293071456, + "docs/api/html/group__test__html__escaping.html": 6092029763861815279, + "venv/lib/python3.13/site-packages/numpy/ma/LICENSE": 6723325434476453127, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_pbkdf2.py": 4977067068389993374, + "backend/src/core/__tests__/test_superset_profile_lookup.py": 12374214375171590188, + "venv/lib/python3.13/site-packages/cffi/cparser.py": 5246372077864988755, + "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.cpython-313-x86_64-linux-gnu.so": 11074560904305737689, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_unittests.py": 18157503533859249155, + "venv/lib/python3.13/site-packages/pygments/styles/material.py": 14325514369696961874, + "venv/lib/python3.13/site-packages/pandas/core/arrays/string_arrow.py": 16997970607998635570, + "venv/lib/python3.13/site-packages/pandas/io/pytables.py": 17482934215513855928, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_item.py": 17619314877770917080, + "docs/api/html/group____require__owner__mutation__scope.html": 370851885631546971, + "docs/api/html/group__TestTokenBudget.html": 11141209479743213441, + "docs/api/html/group__TaskManagerPackage.html": 14112772634468690362, + "venv/lib/python3.13/site-packages/numpy/lib/npyio.pyi": 3212440968296555560, + "docs/api/html/group__get__mapping__service.html": 10008745365667639243, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_complexcompat.h": 5001035774408914877, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nditer.pyi": 33523064068376750, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_array_to_datetime.py": 3467480435568672840, + "docs/api/html/group__register__adfs.html": 7944423328544762483, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayterator.pyi": 7439348655567232744, + "docs/api/html/group__TestClickHouseJoinVerification.html": 7084861590804136181, + "docs/api/html/group__create__git__config.html": 3230144786855183113, + "venv/lib/python3.13/site-packages/pyasn1/debug.py": 6305633719847955310, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray.pyi": 14369009813591437391, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/hist.py": 10928173425078493095, + "backend/src/models/dataset_review_pkg/_enums.py": 501173908863819198, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py": 1945891789924713537, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pxd": 13413785818999584986, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_compat.h": 6512969159139431485, + "docs/api/html/group__handleCheckout__Function.html": 13677007543937370261, + "frontend/src/app.css": 3020720639702954139, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/modes.py": 6832635056228445650, + "docs/api/html/search/groups_d.js": 1277170985754207380, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/build_tracker.py": 9389444024234384408, + "docs/api/html/group__TranslationJob.html": 10104141994060655725, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/_mapping.py": 17519715342700184302, + "docs/api/html/group__test__happy__path.html": 16218633066560416160, + "docs/api/html/group__TranslationMetricsDashboard.html": 15575796997965788450, + "backend/tests/test_translate_scheduler.py": 5320420459889556803, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_attr_equal.py": 5612259834202470413, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_array.py": 12514884344165043136, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/utils.py": 9959199071030996679, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_indexing.py": 2274965639578580723, + "docs/api/html/navtreeindex11.js": 3487316622421924543, + "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.py": 11397521081216402594, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/gh19161.f90": 7691871466158049079, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_conversion.py": 7507083194555424923, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_replace.py": 18039668944453454689, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_impl.py": 16610770935842519289, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/pandas/_libs/tslib.cpython-313-x86_64-linux-gnu.so": 10742615805788605480, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_unique.py": 3148372351781648868, + "venv/lib/python3.13/site-packages/pip/_internal/cli/base_command.py": 10660882478126415399, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/__init__.py": 18036348770966744795, + "frontend/build/_app/immutable/nodes/12.DZAABf7J.js": 6364130532040975768, + "venv/lib/python3.13/site-packages/pandas/_libs/index.pyi": 17679349633640606985, + "docs/api/html/search/all_d.js": 15064235828292081974, + "docs/api/html/group__TranslatePreviewRoutesModule.html": 17344666533968003326, + "backend/src/core/migration/dry_run_orchestrator.py": 1628125044585364194, + "docs/api/html/group__build__banner__text.html": 9722068729315930641, + "docs/api/html/group__test__build__dataset__preview__query__context__preserves__time__range__from__native__filter__payload.html": 814719278183998378, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_neighborhood_iterator_imp.h": 10634781863364525895, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pct_change.py": 10313980265441433173, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resample_api.py": 3278209206670699265, + "docs/api/html/group__SupersetClientFetchAllPages.html": 16697015167259947602, + "backend/src/api/routes/__tests__/test_datasets.py": 5175841745704208723, + "docs/api/html/group__getStorageAuthHeaders__Function.html": 16351303303145823090, + "docs/api/html/group__AuthenticationError.html": 1954448943641493275, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/decoder.py": 11876648884087997745, + "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.pyi": 16799580742094025110, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/RECORD": 3617343778671172258, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/datasource.pyi": 16752667877866430577, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_periodindex.py": 13430003945574795792, + "docs/api/html/group__adminService__Module.html": 6173971485001272907, + "docs/api/html/group__get__current__user.html": 13871827060442489554, + "backend/src/api/routes/__tests__/test_reports_openapi_conformance.py": 16976360004466970131, + "docs/api/html/group__MaintenanceAlreadyActiveResponse.html": 3971170803741620113, + "docs/api/html/group__remove__banner__from__position.html": 10490485317940098773, + "docs/api/html/group____load__module.html": 9106125961389789525, + "docs/api/html/group__add__backup__job.html": 16776188740195692544, + "docs/api/html/group__GitSchemas.html": 7299943490999314464, + "docs/api/html/group__test__map__columns__invalid__source__type.html": 8185525188861927511, + "venv/lib/python3.13/site-packages/_pytest/capture.py": 17850967053989495422, + "docs/api/html/group__TranslateRouterModule.html": 17322360463251819230, + "backend/src/scripts/seed_superset_load_test.py": 7430188463595432869, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/timeseries.py": 17641722582848932332, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/_openssl.pyi": 4376871621615688626, + "venv/lib/python3.13/site-packages/_pytest/terminalprogress.py": 5490989904517343259, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cosh.csv": 9287774220881887502, + "venv/lib/python3.13/site-packages/websockets/legacy/protocol.py": 762692033188392424, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/color_triplet.py": 17264298271771184, + "docs/api/html/group__test__assistant__message__request__accepts__dataset__review__session__binding.html": 17773310738913307655, + "frontend/build/_app/immutable/chunks/kQAbBIK-.js": 18428808609686820865, + "docs/api/html/group__test__get__dashboards__env__not__found.html": 430793696501404989, + "venv/lib/python3.13/site-packages/PIL/SunImagePlugin.py": 7661622930404688367, + "venv/lib/python3.13/site-packages/pandas/api/types/__init__.py": 3995030354191071797, + "venv/lib/python3.13/site-packages/greenlet/TGreenlet.cpp": 4121171004360760212, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets_properties.py": 2340780879864207500, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_filters.py": 17271344092069736999, + "docs/api/html/group____normalize__superset__env__url.html": 13235708743999854328, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filters/__init__.py": 835924536855006596, + "backend/src/plugins/translate/sql_generator.py": 12566523333734311349, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/__init__.py": 11157967642611612522, + "venv/lib/python3.13/site-packages/passlib/handlers/fshp.py": 13298827601291507295, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_stack_unstack.py": 3737891757817696405, + "venv/lib/python3.13/site-packages/pytest_asyncio/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/__init__.py": 4770885427721822507, + "venv/lib/python3.13/site-packages/pydantic/deprecated/config.py": 12280996227531856230, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_constructors.py": 12118296571694122743, + "docs/api/html/group__test__start__idempotency.html": 11136159718052819459, + "docs/api/html/group__clean__release__repositories.html": 4346082490646294177, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html.tpl": 12550218314797699304, + "docs/api/html/group__extract__native__filters__from__key__async.html": 5896768597674544567, + "docs/api/html/group__delete__running__tasks.html": 1080036419250840372, + "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.test.js": 16620540403801513700, + "docs/api/html/group__ExecutionMode.html": 359045115396835597, + "docs/api/html/group__AsyncSupersetClient.html": 8532694979035205515, + "venv/lib/python3.13/site-packages/gitdb/base.py": 7927279446353696534, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows_renderer.py": 1076842273781909680, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/macos.py": 17442393555211213147, + "docs/api/html/group__gitServiceContractTests__Module.html": 15217057486548108664, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_win_type.py": 18105114803686556748, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_nlargest.py": 1423106021399469694, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_keywords.py": 11487954623214475915, + "docs/api/html/group__SupersetContextExtractor.html": 13588033302010707545, + "docs/api/html/group__ClarificationAnswer.html": 11735527821059013045, + "docs/api/html/files_dup.js": 6566197532364479572, + "venv/lib/python3.13/site-packages/pip/_internal/operations/check.py": 16650663913976953800, + "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.py": 3001896031623204147, + "docs/api/html/group__test__create__job__missing__translation__column.html": 9558284213073049757, + "docs/api/html/group__BatchApproveSemanticRequest.html": 5571958815851650821, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_deprecations.py": 14624516389510242394, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/gh18403_mod.f90": 6219951399575860362, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/test_json.py": 12422315190455428620, + "docs/api/html/group__ValidationApiTests.html": 11399974010057798721, + "docs/api/html/group__SourceIntakePanelUxTests__Module.html": 16574757076650646043, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_random.py": 4179188367630060654, + "venv/lib/python3.13/site-packages/httpx/_urlparse.py": 8270294590945652412, + "frontend/src/routes/datasets/__tests__/inline_edit.test.js": 17712409018171053453, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/lambdas.py": 13377298868598623228, + "backend/git_repos/remote/test-repo.git/hooks/pre-receive.sample": 6118492409684431911, + "venv/lib/python3.13/site-packages/passlib/tests/test_apache.py": 12515001998770565375, + "docs/api/html/group__plugins__Data.html": 14082713869477656555, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/WHEEL": 7795541085444298627, + "docs/api/html/group__TaskGraph.html": 14527954946833016645, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_string.py": 13234624995755549329, + "docs/api/html/group__CheckStageName.html": 3993716435006752710, + "docs/api/html/group__loadStatus__Watcher.html": 5559550850267014121, + "docs/api/html/group__test__generate__docs__success.html": 8836757246703621262, + "docs/api/html/group__engine.html": 6333404802173760004, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/android.py": 8607703347015852195, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data": 9599129816090399315, + "backend/src/core/cot_logger.py": 9539001363130629806, + "venv/lib/python3.13/site-packages/uvicorn/server.py": 15714008348292243019, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_internals.py": 10472508540875131797, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_semicolon_split.py": 8153770165829451525, + "venv/lib/python3.13/site-packages/pandas/_testing/compat.py": 10172779814048259850, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_extras.py": 16160090335191788104, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_quarter.py": 11099677280196239755, + "frontend/src/lib/api/translate/runs.js": 1220731879179347641, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py": 6067836157632573182, + "venv/lib/python3.13/site-packages/pygments/styles/native.py": 4559566444883341527, + "venv/lib/python3.13/site-packages/pip/_internal/operations/prepare.py": 13514839972066861359, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.cpython-313-x86_64-linux-gnu.so": 11019871270094058038, + "docs/api/html/group____FakeQuery.html": 2190196221479025836, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctan.csv": 4581185622290535488, + "docs/api/html/group__test__tui__initial__render.html": 3293664978123216509, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_inclusive.py": 1389676378885557519, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/authorization_code.py": 4958605540994687737, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py": 5616705208267050659, + "docs/api/html/group__HAS__PERMISSION__PATTERN.html": 3740860741409953841, + "docs/api/html/group__test__validate__path__creates__directory.html": 7116834460076222248, + "docs/api/html/group__ComplianceRun.html": 6928859539002591467, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/compat.py": 5871636909854554063, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed": 15130871412783076140, + "docs/api/html/group__updateUser__Function.html": 14752881444899728084, + "venv/lib/python3.13/site-packages/numpy/core/einsumfunc.py": 3484247123731380377, + "backend/src/plugins/translate/orchestrator_run_completion.py": 6388710465335071125, + "docs/api/html/group__CONVERSATIONS.html": 17013475999409508111, + "venv/lib/python3.13/site-packages/annotated_types/__init__.py": 7367790549779882749, + "docs/api/html/group__SessionLocal.html": 7893226225561483539, + "venv/lib/python3.13/site-packages/pandas/core/arrays/base.py": 664515424699746697, + "frontend/build/_app/immutable/chunks/C1ZQU4q-.js": 2118208886177086728, + "venv/lib/python3.13/site-packages/httpx/__init__.py": 6383055693166097719, + "docs/api/html/group__AsyncSupersetClientModule.html": 9966218695940620420, + "venv/lib/python3.13/site-packages/passlib/tests/test_context.py": 15195609282700801214, + "docs/api/html/group__TranslateSchedulerTests.html": 5785296020890210045, + "docs/api/html/group__estimate__token__budget.html": 8041209033225986920, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/use_modules.f90": 3913612933340746297, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval.py": 18040371224712437126, + "docs/api/html/group____get__owned__session__or__404.html": 394393571117594920, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_generic.py": 1098361087956787007, + "venv/lib/python3.13/site-packages/pip/_internal/main.py": 13711402795965346761, + "docs/api/html/group__test__parse__dashboard__url__for__filters__no__filters.html": 2766201645946091724, + "docs/api/html/group__handleSort__Function.html": 14047089905040001393, + "docs/api/html/group____resolve__repo__key__from__ref.html": 12707845054172015148, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/arrays/__init__.py": 10005095663840461954, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/RECORD": 6518517239543344179, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape.pyi": 17197248408326188931, + "docs/api/html/group__frontend__src__lib__stores__sidebar__js____setActiveItem.html": 1169479244989646628, + "venv/lib/python3.13/site-packages/gitdb/utils/encoding.py": 1668793658272579497, + "venv/lib/python3.13/site-packages/pydantic/root_model.py": 20625057828723317, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/common.py": 16314033415206173235, + "backend/src/api/routes/__tests__/test_dashboards.py": 3130878778754818679, + "venv/lib/python3.13/site-packages/pygments/lexers/ooc.py": 14540210212631538007, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp2.csv": 10950201682596097452, + "docs/api/html/group__get__tasks__db.html": 282055494774309931, + "venv/lib/python3.13/site-packages/pygments/lexers/verifpal.py": 9190813166686923928, + "docs/api/html/group__test__other__defaults__unchanged.html": 8009088910763350176, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_month.py": 14597470286884431085, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/descriptor_props.py": 7044852927393650707, + "venv/lib/python3.13/site-packages/pip/__pip-runner__.py": 1138979712722199296, + "venv/lib/python3.13/site-packages/numpy/_core/overrides.py": 252940520997718714, + "docs/api/html/group__test__load__schedules__reloads__translation__schedules.html": 16059484179450134258, + "frontend/src/components/git/GitReleasePanel.svelte": 11177244258160406730, + "venv/lib/python3.13/site-packages/pandas/_libs/index.cpython-313-x86_64-linux-gnu.so": 18409579812171549318, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_html.py": 7732330576029516854, + "backend/src/services/clean_release/facade.py": 4588791439430238149, + "docs/api/html/group__register__candidate.html": 9069772319908727023, + "docs/api/html/globals_func_v.html": 13758024740377138009, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_util.py": 13997184496924925110, + "venv/lib/python3.13/site-packages/greenlet/TGreenletGlobals.cpp": 17400526028982558937, + "venv/lib/python3.13/site-packages/PIL/GimpGradientFile.py": 9777026720028891154, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/__init__.py": 6758781485387665020, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_index_tricks.py": 4676293127859087991, + "frontend/src/pages/Settings.svelte": 14062925006358678049, + "frontend/src/services/storageService.js": 9489915460172592752, + "venv/lib/python3.13/site-packages/numpy/typing/__init__.pyi": 10058149460938883683, + "docs/api/html/group__test__compile__dataset__preview__falls__back__to__chart__data__after__legacy__failures.html": 9366582312624823738, + "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py": 968076476163114767, + "backend/src/__init__.py": 11250920420388310655, + "docs/api/html/doxygen__docs_8h.js": 3095005433054142131, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/licenses/LICENSE": 3868190977070717994, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__ResolutionState.html": 10695599682099168307, + "frontend/src/lib/components/settings/ApiKeysTab.svelte": 17208249963941978218, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/claims.py": 11853156183819186599, + "docs/api/html/group__SemanticSource.html": 8217407902309590848, + "frontend/src/routes/settings/+page.ts": 3719008513710037840, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____getTaskDrawerAutoOpenPreference.html": 5828119756126492160, + "docs/api/html/group__SupersetClientParseDashboardUrlForFilters.html": 2985094846647626250, + "venv/lib/python3.13/site-packages/pygments/util.py": 4815334059917765852, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nunique.py": 8450574751331186943, + "venv/lib/python3.13/site-packages/websockets/auth.py": 499195617702591757, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_scalar.py": 2434773821902319678, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_resolution.py": 18198858926948468112, + "venv/lib/python3.13/site-packages/jaraco/functools/__init__.pyi": 2872039192528394677, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_dropna.py": 3620959914439334511, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/_typing.py": 10524021526716767489, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_distutils.pyi": 3595910367556664837, + "docs/api/html/group__RepoStatusBatchRequest.html": 4909634255465475833, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py": 3531093454660385247, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_join.py": 5982692266872978444, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/METADATA": 15987199384245264478, + "docs/api/html/group__DatasetReviewEntryUxPageTests.html": 12544802803136468314, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/operators.f90": 5949265961908386974, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapped_collection.py": 11377280960650304531, + "backend/tests/test_maintenance_api.py": 12900818478424076698, + "docs/api/html/group__dataset__websocket__endpoint.html": 3534578434362936086, + "docs/api/html/group__SupersetContextFiltersExtractMixin.html": 8312542412809507085, + "docs/api/html/group__consolidate__archive__folders.html": 775395364374267476, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_scalar_compat.py": 1306236983392218709, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py": 3867868366067025348, + "docs/api/html/group__artifact__repository.html": 16366748896523529191, + "docs/api/html/group____extract__dialect.html": 17170247248753677907, + "venv/lib/python3.13/site-packages/numpy/core/_dtype.py": 2806449741844148191, + "venv/lib/python3.13/site-packages/passlib/hosts.py": 11171402820797380938, + "docs/api/html/group____apply__features__from__env.html": 5044527824719200918, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_base.py": 16710163879060522196, + "docs/api/html/group__GitOperationsPanel.html": 14155571809658908034, + "venv/lib/python3.13/site-packages/sqlalchemy/util/compat.py": 16148553252010089561, + "frontend/build/_app/immutable/chunks/9TYsDrGm.js": 13284036503586185984, + "venv/lib/python3.13/site-packages/PIL/ImageFilter.py": 10974179993355630141, + "docs/api/html/group__test__unknown__command__returns__needs__clarification.html": 12607016898030176212, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/numerictypes.pyi": 15565573472138344755, + "docs/api/html/group__build__rows.html": 5075247591388659502, + "docs/api/html/group__get__consolidated__settings.html": 18360600092435915493, + "venv/lib/python3.13/site-packages/pandas/tests/construction/test_extract_array.py": 3100523370314150013, + "venv/lib/python3.13/site-packages/rsa/py.typed": 11041940105507257053, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_ticks.py": 11649965080303468085, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/themes.py": 6451091097727599550, + "docs/api/html/group__compute__config__hash.html": 6669149726626922468, + "venv/lib/python3.13/site-packages/websockets/sync/router.py": 10335595955948355382, + "docs/api/html/group__saveLoggingConfig__Function.html": 14654115441279458415, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/LICENSE": 2032205056284080825, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/RECORD": 7266315886702289726, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_map.py": 32086858815359344, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/orm.py": 7554417559838785150, + "venv/lib/python3.13/site-packages/numpy/_typing/_array_like.py": 2161873439361281488, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate_regression.py": 17425202198134409689, + "docs/api/html/navtreeindex5.js": 5360518571003728409, + "venv/lib/python3.13/site-packages/PIL/MicImagePlugin.py": 13540523615822507963, + "venv/lib/python3.13/site-packages/numpy/random/_pcg64.cpython-313-x86_64-linux-gnu.so": 5759445712745875888, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_infer_objects.py": 16868563755487479303, + "docs/api/html/group__test__belief__scope__error__handling.html": 17275160222893974318, + "docs/api/html/group__get__branches.html": 10322130489312145131, + "docs/api/html/group__GitRepoRoutes.html": 10793486578721949471, + "docs/api/html/group__SupersetCompilationAdapter__compile__preview.html": 14843229639835525197, + "venv/lib/python3.13/site-packages/secretstorage/collection.py": 17903258621904912493, + "docs/api/html/group__run__approve.html": 18017929900631845017, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/index_tricks.py": 10051261976498916957, + "venv/lib/python3.13/site-packages/requests/certs.py": 16052880877156864882, + "docs/api/html/group__AssistantApi__Module.html": 4671602144528618803, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_time.py": 10463741033950978270, + "docs/api/html/group__LLMAnalysisModels.html": 10454605905980143274, + "docs/api/html/group__get__environment.html": 1343499401732476775, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame.py": 16757570551941104401, + "docs/api/html/group__authStore__Store.html": 7614884214374261421, + "venv/lib/python3.13/site-packages/pandas/tests/io/conftest.py": 5864037534050136837, + "venv/lib/python3.13/site-packages/pygments/lexers/_asy_builtins.py": 1665617945818958781, + "docs/api/html/group____build__tool__catalog.html": 15194959807685697743, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/__init__.py": 237070345717942041, + "docs/api/html/group__DatabaseMappingsResponse.html": 13473207819975423910, + "venv/lib/python3.13/site-packages/pandas/core/api.py": 3390809109543982410, + "venv/lib/python3.13/site-packages/pandas/core/internals/construction.py": 893951849732478990, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py": 5997687240967250164, + "docs/api/html/group__TestTranslateSchedulerGuard.html": 5632425831791394338, + "venv/lib/python3.13/site-packages/websockets/sync/client.py": 10892557975919033983, + "venv/lib/python3.13/site-packages/apscheduler/triggers/date.py": 9545774812177853725, + "docs/api/html/group__DatasetReviewSessionTests.html": 5186852478551374970, + "docs/api/html/group__handleSubmit__Function.html": 2642372217347398484, + "backend/src/core/utils/superset_context_extractor/_templates.py": 13945363390780279343, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.pyi": 12925006572914814268, + "docs/api/html/group__list__jobs.html": 7585793809542090283, + "backend/src/plugins/translate/__tests__/test_dictionary_prompt_builder.py": 12854863376472707556, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_cpp.cpython-313-x86_64-linux-gnu.so": 7490281502544276964, + "venv/lib/python3.13/site-packages/pygments/lexers/int_fiction.py": 8806961041808606055, + "docs/api/html/group__dataset__review.html": 12837365615967684273, + "docs/api/html/group__test__delete__schedule.html": 16032542899335467172, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite.py": 3628609433179387988, + "docs/api/html/group____collect__src__files.html": 6109138608748972582, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/format": 12697928260201325593, + "dist/docker/ss-tools-frontend.0.1.7.tar.xz": 1586153366853376031, + "venv/lib/python3.13/site-packages/pygments/lexers/iolang.py": 10997512999728724130, + "docs/api/html/group__test__republish__after__revoke__creates__new__active__record.html": 4995611949973079733, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/file_proxy.py": 17695135735211897444, + "docs/api/html/group__ApprovalRequest.html": 11273616744771079528, + "docs/api/html/group__test__backup__dashboards__pre__checks.html": 9786713373502698624, + "docs/api/html/group__assistant__first__message__tests__Function.html": 1507496403746675511, + "venv/lib/python3.13/site-packages/greenlet/greenlet_internal.hpp": 13173115501223013762, + "venv/lib/python3.13/site-packages/yaml/dumper.py": 11376022992370902766, + "docs/api/html/group__ProfilePage.html": 7621039567597119091, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/__init__.py": 8632038387100622009, + "frontend/playwright-report/trace/assets/urlMatch-BYQrIQwR.js": 8070494174996726626, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/database.py": 7111764828004587758, + "docs/api/html/group__run__compliance__report.html": 4432157291692948554, + "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.pyi": 17639497083041129225, + "docs/api/html/group__test__empty__content.html": 11172556345158624109, + "venv/lib/python3.13/site-packages/_pytest/stepwise.py": 3527630676691417317, + "venv/lib/python3.13/site-packages/pandas/tests/api/test_api.py": 13385690673368400635, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/__init__.py": 12290228927283601067, + "frontend/src/routes/datasets/__tests__/ColumnsTable.test.js": 15883951776898228284, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_from_dummies.py": 5670811108950300761, + "frontend/build/_app/immutable/chunks/Bb4u4noz.js": 14646025490063112586, + "docs/api/html/group__health__router.html": 16305034473081450586, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/constants.pyi": 4415188581374004936, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numba.py": 18148155441554555618, + "docs/api/html/group__security__badge__service.html": 15450560198932649899, + "docs/api/html/group__override__detected__language.html": 18199671765025303070, + "venv/lib/python3.13/site-packages/numpy/testing/__init__.pyi": 8126257788928282891, + "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.py": 12886275429547303345, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/codec.py": 14814999235499530522, + "venv/lib/python3.13/site-packages/pydantic/deprecated/decorator.py": 8609500261161560793, + "docs/api/html/group____base__imports.html": 13349247487959032441, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/asyncmy.py": 11808138763443525443, + "venv/lib/python3.13/site-packages/pygments/lexers/graphviz.py": 15677908982985025500, + "venv/lib/python3.13/site-packages/pygments/lexers/clean.py": 17772169928835025327, + "venv/lib/python3.13/site-packages/pygments/lexers/special.py": 4972294202790455038, + "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py": 13386687990085901002, + "docs/api/html/group__run__translation.html": 12662554478381074393, + "docs/api/html/group____generate__banner__id.html": 4235550462258700579, + "docs/api/html/group__DEFAULT__LLM__PROMPTS.html": 7412466426050075586, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.c": 11535375410441493281, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/extensions.py": 217122171232375592, + "venv/lib/python3.13/site-packages/starlette/authentication.py": 10905269573809129547, + "docs/api/html/group__extract__effective__filter__value.html": 5687710115716327783, + "venv/lib/python3.13/site-packages/urllib3/http2/probe.py": 4069929463671970243, + "docs/api/html/group__SupersetClientDeleteDashboard.html": 11929736219574951751, + "backend/src/services/git/_merge.py": 1673121627805278717, + "docs/api/html/group__fetchTargetDatabases__Function.html": 12217542332937976148, + "docs/api/html/group__test__api__key__auth__expired.html": 4953714735118238766, + "docs/api/html/group__LlmRoutes.html": 16633447644830159048, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_describe.py": 11923715539897559626, + "docs/api/html/group__test__ensure__encryption__key__prefers__process__environment.html": 9430354217520222764, + "docs/api/html/group__test__start__check__and__get__status__contract.html": 11086148140670150623, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/sessions.py": 5400174351499383072, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/base.py": 14908084548919567658, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_np_datetime.py": 18363183516409621205, + "venv/lib/python3.13/site-packages/pygments/lexers/jmespath.py": 7426464251945393925, + "backend/src/plugins/mapper.py": 12075913231649127940, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/passlib/context.py": 10362592407593745107, + "docs/api/html/group__frontend__src__services__taskService__js____getTasks.html": 12754837933544376454, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_astype.py": 15768655715952923925, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-2.csv": 7063857529719738394, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cython.py": 14695721638531081573, + "docs/api/html/tab_ad.png": 18360990721051584886, + "venv/lib/python3.13/site-packages/cffi/vengine_cpy.py": 10684192171370688079, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_indexing.py": 6039808271042864401, + "venv/lib/python3.13/site-packages/charset_normalizer/cli/__main__.py": 2148876100501691980, + "venv/lib/python3.13/site-packages/PIL/TiffImagePlugin.py": 6845043550424565106, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_core.py": 12712110303898046818, + "venv/lib/python3.13/site-packages/pycparser/_build_tables.py": 569728086093219621, + "venv/lib/python3.13/site-packages/_pytest/faulthandler.py": 13329320306353718649, + "venv/lib/python3.13/site-packages/passlib/totp.py": 14590474309565233318, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_arithmetic.py": 4858019700859362660, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/METADATA": 1684451390569121499, + "docs/api/html/group__MappingCollectionResponse.html": 393143085518008707, + "docs/api/html/group__test__git__service__get__repo__path__recreates__base__dir.html": 7635897550275628246, + "docs/api/html/group__TranslationEvent.html": 10898166554501315310, + "venv/lib/python3.13/site-packages/psycopg2/_json.py": 9729420372294281405, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/json.py": 14738429697319670035, + "venv/lib/python3.13/site-packages/pygments/lexers/_luau_builtins.py": 13690910306072616818, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets.py": 10923534235697247269, + "venv/lib/python3.13/site-packages/pydantic/json.py": 4109352930301629308, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_libalgos.py": 13934861984871432400, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/METADATA": 5045041742469375466, + "venv/lib/python3.13/site-packages/pip/_internal/commands/download.py": 7533669665002975823, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/_writer.py": 15053365792216907149, + "docs/api/html/navtreeindex10.js": 1322788531203602791, + "docs/api/html/search/mag_sel.svg": 3857167165871619873, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/random.py": 12722750410117529986, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_expanding.py": 12504275097476819918, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_scrypt.py": 8106466515670064013, + "venv/lib/python3.13/site-packages/numpy/lib/_iotools.pyi": 12873243737872788349, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_extension_array_equal.py": 16119106336497630796, + "docs/api/html/group__bulk__find__replace.html": 7341744188539176239, + "docs/api/html/group____validate__artifacts.html": 18436640877945920098, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/__main__.py": 487896753130729554, + "docs/api/html/group__DatasetReviewWorkspace.html": 16920997955167704434, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py": 6152202474302791232, + "frontend/build/_app/immutable/chunks/DRKPts5e.js": 5284577354262819653, + "docs/api/html/group__TestStorageConfigDefaults.html": 2706421657000125549, + "docs/api/html/group__GitMergeDialog.html": 16071754970378737774, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertions.py": 5556667947376168765, + "backend/src/api/routes/environments.py": 6175447476823722833, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_string.py": 10141066226714805009, + "docs/api/html/group__handleNavigate__Function.html": 14764981181056434794, + "venv/lib/python3.13/site-packages/git/repo/__init__.py": 6173604284490891047, + "venv/lib/python3.13/site-packages/pandas/core/indexers/__init__.py": 1756397319068017890, + "docs/api/html/group__TaskManagerModels.html": 10684990442170315567, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/applicator": 6616723520993329538, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_bin_groupby.py": 14831211659124136806, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_week.py": 5756727157937218128, + "docs/api/html/doc.svg": 6703136184032142780, + "docs/api/html/group__frontend__src__services__storageService__js____getStorageAuthHeaders.html": 18312324080831107841, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_timer.py": 8196003526646080498, + "docs/api/html/group__test__database__url__raises__when__both__unset.html": 842987545358384743, + "venv/bin/pyrsa-decrypt": 10929163076272054227, + "venv/lib/python3.13/site-packages/pygments/lexers/solidity.py": 5071425960622305629, + "venv/lib/python3.13/site-packages/pygments/lexers/installers.py": 5083244110640899732, + "docs/api/html/group__test__configure__logger__post__conditions.html": 10707062206466040317, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/RECORD": 4857922668040624663, + "venv/bin/activate": 14735074000123383125, + "backend/src/plugins/translate/__tests__/test_executor.py": 9363667239624864820, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/REQUESTED": 15130871412783076140, + "docs/api/html/group__ApiKeyListItem.html": 5748587586873326651, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_frame_equal.py": 12459816298158655274, + "venv/pyvenv.cfg": 2513607356761047446, + "venv/lib/python3.13/site-packages/pydantic/v1/utils.py": 902648247561165429, + "frontend/src/routes/datasets/+page.svelte": 6456061135946032594, + "backend/git_repos/remote/test-repo.git/hooks/prepare-commit-msg.sample": 7530253302780245896, + "docs/api/html/group__MaintenanceStore__methods.html": 7821050348134458375, + "docs/api/html/group__TaskPersistenceService.html": 12977647248776427832, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/expression.py": 15511250785671695359, + "docs/api/html/group__mock__env__public__Module.html": 11925751473549536485, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/timeout.py": 1807228452024740476, + "docs/api/html/group__test__root__path__default.html": 3749097591798015561, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____updateResourceTask.html": 10319226416860236365, + "venv/lib/python3.13/site-packages/gitdb/util.py": 645937410168583692, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/REQUESTED": 15130871412783076140, + "docs/api/html/group__src__plugins.html": 5004920650497586279, + "docs/api/html/group__test__cors__parsing__with__trailing__spaces.html": 2388414963323398944, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_arrow_string_mixins.py": 16862663427780315543, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_subclass.py": 13490541871827928531, + "frontend/build/_app/immutable/nodes/37.DId6O7Ra.js": 16919241507461305725, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/attr/_funcs.py": 5302917072419624193, + "docs/api/html/search/groups_10.js": 10270802865887856312, + "docs/api/html/group__test__update__git__config__modifies__record.html": 2052816531421607126, + "docs/api/html/group__frontend__src__services__git__utils__js____resolvePushProviderLabel.html": 7839804684458326617, + "venv/lib/python3.13/site-packages/_pytest/runner.py": 5772610501857496741, + "docs/api/html/group__onMount__Function.html": 15261809730593069613, + "docs/api/html/group____extract__result__deep__links.html": 2321729088905700625, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_skew.py": 5276102682005383261, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_common.py": 10797243274520625867, + "docs/api/html/group__MAX__OUTPUT__HEADROOM.html": 8679042542420488178, + "venv/lib/python3.13/site-packages/pygments/styles/inkpot.py": 10516710471319090846, + "frontend/playwright-report/trace/uiMode.html": 2646979321132705707, + "venv/lib/python3.13/site-packages/pygments/lexers/smalltalk.py": 6113431347120223592, + "docs/api/html/group__test__trigger__validation__skips__missing__policy.html": 5602615003618981152, + "venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py": 7733356903092860495, + "venv/lib/python3.13/site-packages/requests/cookies.py": 4997226307650350876, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_to_xarray.py": 15341013524814290960, + "docs/api/html/group__test__publish__without__approval__rejected.html": 3363942287403935607, + "docs/api/html/group__TestTranslateSchedulerExecution.html": 685214232569505268, + "docs/api/html/group__test__jinja__set__block.html": 1391771195083382074, + "docs/api/html/group__DashboardProjection.html": 9806475663684458617, + "docs/api/html/group__TranslationLanguageResponse.html": 17228524673941050506, + "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__reference.html": 7880890145565202782, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.cpython-313-x86_64-linux-gnu.so": 2574035311375507054, + "venv/lib/python3.13/site-packages/uvicorn/__main__.py": 6086938803696646644, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.obj": 2884737706935637437, + "venv/lib/python3.13/site-packages/pip/__main__.py": 574438877257676911, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_cut.py": 7800833253841657380, + "docs/api/html/group____resolve__dashboard__id__from__ref.html": 3342456016478632916, + "venv/lib/python3.13/site-packages/pandas/core/groupby/numba_.py": 15997665748332235683, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_openpyxl.py": 9542840527483618681, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_parser.py": 8145921552236189367, + "docs/api/html/group__SupersetClientExtractOwnerLabels.html": 12596315108945733263, + "backend/tests/test_task_manager.py": 1283249534484796250, + "docs/api/html/group__test__load__tasks__with__status__filter.html": 12175575007475513280, + "docs/api/html/group__validate__job__preconditions.html": 8970079820851005162, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/wheel.py": 13979536782410081712, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_asof.py": 15792528637923603240, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/__init__.py": 15130871412783076140, + "docs/api/html/group__test__validate__path__nonexistent__root.html": 15516438199424145609, + "docs/api/html/group__test__simple__select.html": 7841602152481376118, + "docs/api/html/group__deleteRun__Function.html": 15413700130406237535, + "docs/api/html/group____EXT__frontend__ReportTypeProfiles__Test__Module.html": 4055177384498479214, + "backend/src/services/clean_release/repositories/artifact_repository.py": 7507938934134519998, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_riscv_unix.h": 8607317131295864391, + "docs/api/html/group__DatasourceColumnResponse.html": 13651374436200605439, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py": 14865906347579451546, + "frontend/src/routes/datasets/__tests__/DatasetPreview.test.js": 12281160027416292128, + "docs/api/html/search/all_14.js": 8771148315983143295, + "venv/lib/python3.13/site-packages/PIL/ImImagePlugin.py": 15452956038794399492, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/literal.py": 5641382379370828704, + "docs/api/html/group__frontend__src__services__adminService__js____getLoggingConfig.html": 10395249450462043320, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/distributions.h": 17162252287980242413, + "backend/src/plugins/llm_analysis/service.py": 7341173732160910490, + "docs/api/html/group__test__no__end__time__skipped.html": 6670304755300596026, + "docs/api/html/group__test__list__runs__includes__language__info.html": 6499374490278250016, + "venv/lib/python3.13/site-packages/_pytest/debugging.py": 16135285365306979060, + "venv/lib/python3.13/site-packages/pip/py.typed": 10243553515949422671, + "venv/lib/python3.13/site-packages/pandas/core/sample.py": 11913688303879867963, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_dict_compat.py": 18196543090455290455, + "venv/lib/python3.13/site-packages/pygments/styles/vim.py": 2621036947954523349, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/RECORD": 15865206372525596795, + "backend/src/models/filter_state.py": 9299838116029912439, + "frontend/src/lib/api/translate/corrections.js": 3619404083693055292, + "docs/api/html/nav_g.png": 6882746348872232672, + "docs/api/html/group__formatDate__Function.html": 15997975478657062019, + "venv/lib/python3.13/site-packages/fastapi/background.py": 16533963020573865222, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_structures.py": 16375837477294135345, + "docs/api/html/group__test__async__network__404__mapping__keeps__non__dashboard__endpoints__generic.html": 15980107135135079303, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py": 5324490103277723813, + "venv/lib/python3.13/site-packages/fastapi/openapi/utils.py": 9058442333718187759, + "venv/lib/python3.13/site-packages/pygments/lexers/x10.py": 6167378515780115912, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_list_accessor.py": 8613662386779664039, + "backend/src/plugins/llm_analysis/scripts/superset_auth_diag.py": 9191197462223794195, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_quantile.py": 11966156706220948315, + "venv/lib/python3.13/site-packages/greenlet/platform/setup_switch_x64_masm.cmd": 8473866644286785815, + "frontend/src/lib/ui/Input.svelte": 1114920008107220678, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.py": 13853220736499979380, + "docs/api/html/group__TaskLogPanel.html": 14846603073845295002, + "venv/lib/python3.13/site-packages/git/refs/reference.py": 2038789918710608481, + "venv/lib/python3.13/site-packages/requests/__version__.py": 6677532709758546816, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_bcrypt.py": 11146468670439284889, + "venv/lib/python3.13/site-packages/numpy/__init__.pxd": 13527602429695748857, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_to_timestamp.py": 17572983799929378095, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/__init__.py": 15130871412783076140, + "docs/api/html/group__test__list__active__schedules.html": 11816123497638243525, + "venv/lib/python3.13/site-packages/pycparser/lextab.py": 16045770806700381661, + "venv/lib/python3.13/site-packages/pygments/styles/gruvbox.py": 9489017112545247388, + "venv/lib/python3.13/site-packages/_pytest/stash.py": 14430252294763173610, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/api.py": 9829629144629841277, + "venv/lib/python3.13/site-packages/websockets/sync/__init__.py": 15130871412783076140, + "docs/api/html/group__update__dashboard__layout.html": 2290775089034614539, + "docs/api/html/group__MaintenanceStartRequest.html": 13433532661036125704, + "venv/lib/python3.13/site-packages/starlette/config.py": 16509394679506300256, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_missing.py": 1963705411068820866, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/METADATA": 11439969143853665311, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.pyi": 1826323011169529229, + "docs/api/html/group__TriggerRunResponse.html": 2460487645521789314, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/pager.py": 18072372651585577904, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_chebyshev.py": 8054411443686791426, + "docs/api/html/group__CorrectionSubmitResponse.html": 6015523326141419112, + "docs/api/html/group__clean__release__mappers.html": 5318141435059670889, + "venv/lib/python3.13/site-packages/websockets/asyncio/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_configtool.py": 2946953194004101078, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_datetime.py": 7862725028865965163, + "venv/lib/python3.13/site-packages/httpcore/_sync/connection_pool.py": 8159971229245441880, + "docs/api/html/group__PluginsRouter.html": 17546343283725473848, + "venv/lib/python3.13/site-packages/dotenv/ipython.py": 15904966766060247908, + "backend/src/plugins/translate/plugin.py": 15783602737040717978, + "docs/api/html/group__service.html": 13599013976618262143, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_astype.py": 16700461036658228009, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_set_value.py": 4166411740676564820, + "backend/src/services/clean_release/candidate_service.py": 12771065368619689666, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/intranges.py": 12536174834761591006, + "venv/lib/python3.13/site-packages/urllib3/__init__.py": 1854778274753799815, + "backend/test.db": 15130871412783076140, + "docs/api/html/group__test__content__with__padding.html": 3201553403801844016, + "frontend/build/_app/immutable/nodes/38.xBkgH3fw.js": 12418121151792137902, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_dtype.py": 1647214604678171565, + "venv/lib/python3.13/site-packages/git/objects/fun.py": 15014209679156260865, + "docs/api/html/group__CoreContracts.html": 17692523189416853827, + "backend/src/services/clean_release/audit_service.py": 13892224612412540989, + "docs/api/html/group__MaintenanceEndAllTaskResult.html": 2276064404712323484, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/INSTALLER": 17282701611721059870, + "backend/src/models/clean_release.py": 13148726684938514538, + "docs/api/html/group__TestGitServiceGiteaPr.html": 5702873534344534880, + "docs/api/html/group__test__tui__real__mode__bootstrap__imports__artifacts__catalog.html": 6102024140189662129, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_filter.py": 2388681477981649268, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/RECORD": 963865046004908868, + "venv/lib/python3.13/site-packages/packaging/requirements.py": 2896326326372719999, + "backend/src/plugins/translate/__tests__/test_dictionary_correction.py": 12863335297536023159, + "venv/lib/python3.13/site-packages/numpy/_core/lib/libnpymath.a": 17555816778997028283, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/strings.pyi": 402863478592154500, + "venv/lib/python3.13/site-packages/starlette/concurrency.py": 9835859079437525910, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_npfuncs.py": 8238385897366878558, + "venv/lib/python3.13/site-packages/sqlalchemy/util/topological.py": 11586868018611359415, + "docs/api/html/group__make__adapter.html": 7726861153656207298, + "venv/lib/python3.13/site-packages/pandas/core/arrays/period.py": 11560984428402318984, + "frontend/src/lib/components/dataset-review/ValidationFindingsPanel.svelte": 6548767763019176627, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/_cocoa_builtins.py": 15468347167912293394, + "docs/api/html/group__ClarificationOptionDto.html": 16094217856744339801, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/asyncio.py": 4200928269459447593, + "docs/api/html/group__PermissionSchema.html": 6474689152156131373, + "venv/lib/python3.13/site-packages/pygments/formatters/irc.py": 2133711525226123259, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_datetimelike.py": 17304541521108700317, + "frontend/svelte.config.js": 8473636210766821560, + "venv/lib/python3.13/site-packages/pandas/compat/numpy/__init__.py": 1037113402100731082, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein_py.py": 11350239659973489646, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_constructors.py": 12628643543371952431, + "venv/lib/python3.13/site-packages/referencing/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timestamp.py": 10308799534491190078, + "venv/lib/python3.13/site-packages/greenlet/TGreenlet.hpp": 6993553444981671786, + "docs/api/html/group__deleteUser__Function.html": 7949308553108288142, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_jsonschema_test_suite.py": 12733761462141495972, + "venv/lib/python3.13/site-packages/pygments/lexers/sas.py": 6128008194712914874, + "backend/src/api/routes/__tests__/test_reports_api.py": 15567445657565275874, + "backend/git_repos/remote/test-repo.git/objects/d8/3fe0db1279ef07c4c4f3de4156a629373b4057": 7507193819578792576, + "venv/lib/python3.13/site-packages/pandas/api/indexers/__init__.py": 17431573013142626187, + "backend/src/core/superset_client/_user_projection.py": 15240732089847883635, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/INSTALLER": 17282701611721059870, + "frontend/src/routes/settings/__tests__/settings_page.ux.test.js": 14507112961464496362, + "backend/src/services/sql_table_extractor.py": 3035779605324351849, + "venv/lib/python3.13/site-packages/pandas/tests/test_algos.py": 11860014896449369497, + "venv/lib/python3.13/site-packages/pydantic/v1/schema.py": 10008526732895436361, + "frontend/src/routes/datasets/ColumnsTable.svelte": 14113713230812728863, + "docs/api/html/group__test__json__load__if__needed__plain__string.html": 16000096952478615435, + "backend/tests/test_smoke_app.py": 18091565209740001486, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/abc.py": 6227782083136374888, + "venv/lib/python3.13/site-packages/pygments/styles/algol_nu.py": 6229131016121450375, + "venv/lib/python3.13/site-packages/annotated_doc/py.typed": 15130871412783076140, + "backend/src/services/llm_provider.py": 17053465522943758939, + "venv/lib/python3.13/site-packages/numpy/random/lib/libnpyrandom.a": 1802449692091040396, + "docs/api/html/group__test__api__key__auth__valid.html": 3238968940529899361, + "venv/lib/python3.13/site-packages/numpy/_core/strings.pyi": 1981676808027146409, + "docs/api/html/group__CleanComplianceOrchestrator.html": 1417516327802744207, + "frontend/playwright-report/trace/xtermModule.DYP7pi_n.css": 9346562810570076139, + "venv/lib/python3.13/site-packages/starlette/websockets.py": 13834731343216764204, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh15035.f": 13126319439858352711, + "backend/src/app.py": 10622204150857884050, + "backend/src/services/clean_release/enums.py": 5838176998590033780, + "backend/src/services/mapping_service.py": 14740129302452546978, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_functions.py": 2255133375699086300, + "docs/api/html/group____build__default__preference.html": 12609186263613185575, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_floatingpoint_errors.py": 4301355720042319835, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_find_replace.py": 262184394614748386, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____pollStatus.html": 17320763933447233448, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py": 1197579313959289096, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/inferno.py": 9056274496469409594, + "venv/lib/python3.13/site-packages/keyring/backend_complete.zsh": 14339032329370660432, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/__init__.py": 15130871412783076140, + "backend/src/models/report.py": 12105030884508258995, + "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.pyi": 15848834171197104301, + "docs/api/html/group__get__next__executions.html": 11223768021455297815, + "venv/lib/python3.13/site-packages/pygments/lexers/tlb.py": 1169242051301155885, + "backend/src/plugins/translate/service_inline_correction.py": 14126835317134991919, + "venv/lib/python3.13/site-packages/keyring/backends/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pytest/__init__.py": 6137272437722350673, + "docs/api/html/group__TemplateVariable.html": 1861586953213650229, + "venv/lib/python3.13/site-packages/_pytest/config/argparsing.py": 11156052029258891503, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply.py": 8103417303073393969, + "docs/api/html/group__frontend__src__lib__api__validation__js____duplicateTask.html": 9243462936463003663, + "docs/api/html/group__handlePasswordResume__Function.html": 9536732305476854074, + "venv/lib/python3.13/site-packages/keyring/backends/libsecret.py": 11621088556661567306, + "docs/api/html/group__formatTime__Function.html": 12061565145373670587, + "docs/api/html/group__handlePasswordPrompt__Function.html": 13969204145161370143, + "docs/api/html/group__test__put__settings.html": 6582837714267844402, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/__init__.pyi": 10548370574281981402, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_normalize.py": 7378656958631172603, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/index.py": 15694995431926521471, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/util.py": 955993052529739164, + "venv/lib/python3.13/site-packages/idna/codec.py": 4260241549062101779, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/sphinxext.py": 2403717774564798878, + "venv/lib/python3.13/site-packages/pandas/core/tools/__init__.py": 15130871412783076140, + "docs/api/html/group__FuzzyMatching.html": 7818943824809763950, + "docs/api/html/group__configure__logger.html": 6165988274497149791, + "docs/api/html/group__BackupsPage.html": 927182552608523101, + "venv/lib/python3.13/site-packages/pandas/io/json/_json.py": 1393095374713367445, + "docs/api/html/group__test__preview__falls__back__across__matrix__until__supported__endpoint__returns__sql.html": 14400928836202677761, + "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.pyi": 6072607880689125113, + "backend/src/services/resource_service.py": 6963059248749738955, + "backend/git_repos/remote/test-repo.git/refs/heads/preprod": 2288780285329550368, + "docs/api/html/nav_hd.png": 5189739344381957223, + "docs/api/html/group__batch__detect.html": 7272748616355423819, + "docs/api/html/group__get__dashboard__layout.html": 16387331877683303616, + "docs/api/html/group__ComplianceOrchestrator.html": 1898684865008336902, + "docs/api/html/group__IdMappingServiceModule.html": 13805743174711201381, + "docs/api/html/group__TaskResultPanel.html": 4837205169025137611, + "docs/api/html/group__getRoles__Function.html": 4843900854760058913, + "frontend/tests/maintenance-api.test.ts": 10196278218740408254, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/dml.py": 6567627855667533645, + "venv/lib/python3.13/site-packages/pillow.libs/libfreetype-ee1c40c4.so.6.20.4": 12301510769231120312, "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.py": 16854377502491291093, - "venv/lib/python3.13/site-packages/fastapi/params.py": 14263655860695381481, - "backend/src/plugins/translate/executor.py": 15866803979457728942, + "docs/api/html/group__task__id.html": 14850347989291467417, + "frontend/build/_app/immutable/nodes/0.mtrsy6kx.js": 10256856601732374603, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/nested_secrets.py": 15691917061715429369, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_ranges.py": 2572903460327860111, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_strings.py": 7379299916213794543, + "venv/lib/python3.13/site-packages/uvicorn/py.typed": 15240312484046875203, + "docs/api/html/group__openAssistantChat__Function.html": 13650659288497145081, + "venv/lib/python3.13/site-packages/anyio/_core/_tasks.py": 14104828341875599130, + "backend/src/api/routes/translate/_correction_routes.py": 9348235443422223333, + "venv/lib/python3.13/site-packages/numpy/conftest.py": 8971879716683736515, + "backend/src/core/auth/__tests__/test_auth.py": 6524647391979341734, + "venv/lib/python3.13/site-packages/pygments/modeline.py": 6194365452131561478, + "venv/lib/python3.13/site-packages/PIL/ImageColor.py": 17058618058554970738, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/util.py": 5252158959761945519, + "docs/api/html/group__login__adfs.html": 10102492706907150145, + "docs/api/html/globals_func_h.html": 17098238941487002581, + "docs/api/html/globals_func_u.html": 9477828494895601314, + "docs/api/doxygen_stripped.c": 14291099198142844291, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/subversion.py": 7423946096970870727, + "docs/api/html/group__MigrationDryRunService.html": 12712480299293943779, + "docs/api/html/group__TestTextCleaner.html": 16532067994218161464, + "venv/lib/python3.13/site-packages/annotated_types/test_cases.py": 5123395674810472016, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_serialization.py": 2011519400081029270, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_is_monotonic.py": 12769599198035357753, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_series.pyi": 7112486730197067087, + "docs/api/html/group__test__backup__dashboards__env__not__found.html": 8478433620760041140, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_ipython_compat.py": 12632271159842988561, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/well_known.py": 8259335680633926377, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_xport.py": 14080914637071212225, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/test_pyinstaller.py": 9430762168079129025, + "docs/api/html/navtreeindex24.js": 8251711078163677251, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/asyncio.py": 2517623574187179319, + "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.pyi": 15067467792538416274, + "venv/lib/python3.13/site-packages/jsonschema/validators.py": 3832874295500213309, + "venv/lib/python3.13/site-packages/PIL/_imagingtk.cpython-313-x86_64-linux-gnu.so": 1708198947135304326, + "venv/lib/python3.13/site-packages/authlib/oauth1/errors.py": 1154126353335030046, + "docs/api/html/group__MergeStatusSchema.html": 12600052237122559158, + "docs/api/html/group__test__derive__server__url__from__remote__strips__credentials.html": 11245056897250870095, + "docs/api/html/group__reject__candidate__endpoint.html": 15716705953047471464, + "docs/api/html/group__GitHelpers.html": 3500975807832633271, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__init__.py": 8880435562116660648, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_round.py": 8336918400205461173, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_npy_units.py": 8966226962799105383, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/WHEEL": 7414522633964952515, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress.py": 5951745251426856398, + "venv/lib/python3.13/site-packages/numpy/f2py/rules.pyi": 1342380029484127983, + "venv/lib/python3.13/site-packages/click/utils.py": 13475604420010022458, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_frame.py": 4303613319747945709, + "backend/src/plugins/translate/__tests__/test_text_cleaner.py": 5456979246104056266, + "docs/api/html/group__handleInitializeRepositories__Function.html": 4127737207323707916, + "docs/api/html/search/all_1.js": 5807190646834111663, + "venv/lib/python3.13/site-packages/pip/_internal/cli/req_command.py": 2887890799766730546, + "docs/api/html/group__test__task__logger__initialization.html": 11239153689675430218, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fromnumeric.pyi": 8671744104085911610, + "docs/api/html/group__frontend__src__services__adminService__js____createRole.html": 8909533477901199154, + "docs/api/html/group__test__empty__input.html": 17644334770081506611, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/__init__.py": 10072943986583771248, + "venv/lib/python3.13/site-packages/pygments/lexers/nix.py": 15050086330545600356, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timedeltas.py": 1175883320586860493, + "docs/api/html/group__test__git__default__repos__path__constant.html": 5012382907811240841, + "docs/api/html/group__generate__docs.html": 13997260304920699682, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_processors.py": 16557117595879208702, + "venv/lib/python3.13/site-packages/more_itertools/recipes.pyi": 16788810745659802820, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/libdivide.h": 5314232500579481313, + "frontend/src/routes/datasets/review/[id]/__tests__/dataset_review_workspace.ux.test.js": 15632955695079004160, + "backend/src/api/routes/git_schemas.py": 9763256396006436721, + "venv/lib/python3.13/site-packages/pip/_internal/locations/__init__.py": 8147012724727395488, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_core_functionalities.py": 10449747865888948579, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_antijoin.py": 14435396877404907826, + "venv/lib/python3.13/site-packages/websockets/__init__.py": 4916307256304054040, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/mock.py": 14010776046610111182, + "venv/lib/python3.13/site-packages/pygments/lexers/tcl.py": 10343667975436015855, + "venv/lib/python3.13/site-packages/pip/_internal/utils/hashes.py": 17310331665429541812, + "venv/lib/python3.13/site-packages/charset_normalizer/models.py": 9917300217816275821, + "docs/api/html/group____discover__plugin__execute__permissions.html": 14000239163315189840, + "docs/api/html/group__download__file.html": 17674094830144003140, + "backend/alembic/versions/b0c1d2e3f4a5_cascade_ondelete_translate_fks.py": 8121802502285465603, + "venv/lib/python3.13/site-packages/anyio/streams/tls.py": 4191898645554875562, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/WHEEL": 5838667082726674838, + "venv/lib/python3.13/site-packages/jeepney/wrappers.py": 15234251727654692435, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_msvc.h": 2465089365642299646, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.asm": 15349921092257597740, + "docs/api/html/search/functions_12.js": 2064688989625290905, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_timedelta.py": 364160935264757405, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/endpoint.py": 8519226897299461194, + "venv/lib/python3.13/site-packages/numpy/_core/memmap.pyi": 14124415711399771715, + "venv/lib/python3.13/site-packages/sqlalchemy/__init__.py": 9751988289698484530, + "docs/api/html/group__SqlLabLaunchPayload.html": 7835290887584234240, + "docs/api/html/group__test__superset__api__failure.html": 13822775890788292618, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/WHEEL": 12146818530001975731, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/rec.pyi": 9140655366667421698, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_partial_slicing.py": 16219611534574666182, + "docs/api/html/group____seed__candidate__with__passed__report.html": 16678234707349492648, + "docs/api/html/group__seed__permissions.html": 4126130443403331361, + "venv/lib/python3.13/site-packages/pygments/lexers/zig.py": 470431199354307668, + "docs/api/html/group____serialize__session__detail.html": 616460683247422819, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/__init__.py": 16434403703599680307, + "docs/api/html/group__test__banner__not__found.html": 8635777298336273949, + "docs/api/html/group__submit__bulk__corrections.html": 17842098461150569742, + "docs/api/html/group__Footer.html": 12554974943803824206, + "frontend/src/routes/migration/+page.svelte": 18384061091388956338, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/introspection.py": 5447179017910128771, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/resource_protector.py": 9147671710440653567, + "venv/lib/python3.13/site-packages/pygments/lexers/ncl.py": 14838368780033177943, + "docs/api/html/group__NativeFilterDataMask.html": 16691586180906605871, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/METADATA": 7214286447912789067, + "venv/lib/python3.13/site-packages/pandas/io/formats/style_render.py": 13586834368514801953, + "docs/api/html/group____extract__quoted__segment.html": 14177192301285608602, + "docs/api/html/group__get__health__summary.html": 12658858476431678972, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_function_base.pyi": 10972502518588022706, + "docs/api/html/group__SupersetContextFiltersExtractMixin____extract__imported__filters.html": 5149922330981040989, + "docs/api/html/group__SeedSupersetLoadTestScript.html": 2748544528766669921, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_reshape.py": 5572426834735088916, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_duplicate_labels.py": 10663105616336167414, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.pyx": 649821942746469002, + "docs/api/html/group__extract__objects__from__zip.html": 4781757022927901861, + "venv/lib/python3.13/site-packages/_pytest/config/exceptions.py": 3199245296954654462, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pop.py": 14885271225856187795, + "docs/api/html/group__get__report__detail.html": 11469102499923216293, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/conftest.py": 17514079438846557943, + "venv/lib/python3.13/site-packages/pillow.libs/libwebpdemux-747f2b49.so.2.0.17": 7615380692384566654, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/__init__.py": 63497920264089252, + "venv/lib/python3.13/site-packages/pandas/core/groupby/categorical.py": 16588862198627920420, + "backend/src/api/routes/assistant/_history.py": 6282663866884159556, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/base.py": 17992335652981439676, + "docs/api/html/group__is__adfs__configured.html": 6773671149149811294, + "docs/api/html/group__resolve__provider__model.html": 3697548583109098137, + "venv/lib/python3.13/site-packages/greenlet/TBrokenGreenlet.cpp": 10822436772777706192, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/wrappers.py": 315409799569602977, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_cell_widths.py": 5724379364993806351, + "docs/api/html/group__clean__release__enums.html": 3886475013764944523, + "docs/api/html/group__TestDictionaryPromptBuilder.html": 990574254125574660, + "docs/api/html/group__remove__tasks.html": 11302233214919049269, + "docs/api/html/group__ValidationPolicyUpdate.html": 14465919311238997171, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/well_known.py": 7183795183137931709, + "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.pyi": 7171810177801103765, + "venv/lib/python3.13/site-packages/pandas/_libs/json.pyi": 10989301892466547548, + "docs/api/html/group__SchemasPackage.html": 18430871611068960110, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/specifiers.py": 12284901056355482991, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/pytestplugin.py": 1591034895268415944, + "docs/api/html/group__ConflictResolution.html": 8175611913139451582, + "venv/lib/python3.13/site-packages/smmap/test/test_buf.py": 6910367739329016619, + "frontend/src/components/__tests__/task_log_viewer.test.js": 4344163242946147110, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_sparse_accessor.py": 6774374030220202353, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE": 17284252174641192866, + "venv/lib/python3.13/site-packages/httpcore/_async/http11.py": 7565741048880549265, + "venv/lib/python3.13/site-packages/greenlet/greenlet.cpp": 3281107194043468270, + "docs/api/html/group__frontend__src__lib__stores__sidebar__js____toggleMobileSidebar.html": 2764792336812485289, + "docs/api/html/doxygen__docs_8h_source.html": 6115156353195135130, + "docs/api/html/group____serialize__clarification__state.html": 16624968641905184426, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/METADATA": 12295263493152296681, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_histograms.py": 47631283818191049, + "docs/api/html/group__disconnectWebSocket__Function.html": 8352221169150959669, + "backend/src/api/routes/__tests__/test_git_api.py": 4133163122739824202, + "docs/api/html/group__CandidateArtifact.html": 1360984683406873421, + "venv/lib/python3.13/site-packages/pygments/lexers/urbi.py": 1611762607688882372, + "docs/api/html/group__PROMPT__BASE__TOKENS.html": 625575964554485020, + "docs/api/html/group__generate__insert__sql.html": 10581163190354294492, + "venv/lib/python3.13/site-packages/yaml/reader.py": 9873075843010693205, + "backend/src/api/routes/translate/_dictionary_routes.py": 5668671879929670471, + "docs/api/html/group__AssistantCommandParser.html": 5740092626323461301, + "docs/api/html/group__test__native__filter__data__mask__model.html": 14477752184631397987, + "docs/api/html/group__TranslationPreviewSession.html": 709141522736963142, + "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py": 5583478355093966990, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL": 18203500019759199992, + "venv/lib/python3.13/site-packages/pandas/core/reshape/api.py": 16364136812252343076, + "docs/api/html/globals_func_b.html": 7416406823814080837, + "venv/lib/python3.13/site-packages/websockets/exceptions.py": 12048063047468402740, + "venv/lib/python3.13/site-packages/numpy/random/_generator.cpython-313-x86_64-linux-gnu.so": 15625019745021657670, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/expressions.py": 8015786996064244656, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/RECORD": 11788532857617688351, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/bitwise_ops.py": 14350609278188982939, + "venv/lib/python3.13/site-packages/_pytest/raises.py": 8188402179490363767, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/__init__.py": 1798074863773727377, + "venv/bin/httpx": 6651007446865716794, + "docs/api/html/group____FakeQueryDel.html": 12251734858726214042, + "venv/lib/python3.13/site-packages/pandas/_libs/parsers.pyi": 18402185996588014111, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/request.py": 17642741955458391050, + "docs/api/html/group__FeedbackRequest.html": 5229199263614285726, + "docs/api/html/group__DataPurityStage.html": 13800816093664302434, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_coercion.py": 3339075591248270762, + "venv/lib/python3.13/site-packages/psycopg2/_psycopg.cpython-313-x86_64-linux-gnu.so": 13471342289207537479, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_first_valid_index.py": 15763400665851423183, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_skiprows.py": 8764515842253490735, + "frontend/build/_app/immutable/chunks/8jst63yt.js": 10345850741459634233, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/WHEEL": 9100692507274722091, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/apply.py": 3906017176873949738, + "venv/lib/python3.13/site-packages/numpy/f2py/rules.py": 9322914583460630877, + "docs/api/html/group____normalize__dashboard__owner__values.html": 5450496100604102874, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pyx": 11739218099400096619, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_na_scalar.py": 13950899907067372014, + "venv/lib/python3.13/site-packages/pygments/lexers/hexdump.py": 10665271113239513484, + "docs/api/html/group__SessionSummary.html": 13869105024431340740, + "venv/lib/python3.13/site-packages/pygments/lexers/typoscript.py": 6706632522710582762, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py": 9925183787411796679, + "venv/lib/python3.13/site-packages/pandas/core/util/hashing.py": 13106575357604888913, + "venv/lib/python3.13/site-packages/pandas/util/_validators.py": 10201915528715968165, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_halfyear.py": 587199217232946404, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed25519.py": 2285778903282667561, + "venv/lib/python3.13/site-packages/uvicorn/middleware/message_logger.py": 14825516604555033580, + "docs/api/html/group__MaintenanceEventItem.html": 15520444473677444041, + "docs/api/html/group__ComplianceViolation.html": 2732715043898056869, + "venv/lib/python3.13/site-packages/starlette/middleware/trustedhost.py": 8302185291610019926, + "venv/lib/python3.13/site-packages/pygments/styles/solarized.py": 11231143047572616298, + "venv/lib/python3.13/site-packages/pygments/lexers/srcinfo.py": 16466766917840327610, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/thingsdb.py": 6073447481627395832, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/collections.py": 16114931075157411913, + "venv/lib/python3.13/site-packages/pygments/lexers/_openedge_builtins.py": 4979988747924637739, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_factorize.py": 17995673140216929494, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq_py.py": 16054175754674285311, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_setitem.py": 10389770446834773837, + "docs/api/html/group__LLMClient.html": 17661474179652909834, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_construction.py": 13953424340346625390, + "docs/api/html/group__test__get__task__logs__not__found.html": 3813624608832592211, + "docs/api/html/group__SupersetClientExtractChartIdsFromLayout.html": 397319256700010303, + "venv/lib/python3.13/site-packages/pandas/core/indexers/utils.py": 5861072573716897447, + "docs/api/html/group__get__app__timezone.html": 1707039123202823020, + "venv/lib/python3.13/site-packages/PIL/ImageFont.py": 15621336334706847865, + "docs/api/html/group__test__cors__parsing__single__value.html": 5377998052395946901, + "docs/api/html/group__handleTestEnv__Function.html": 9541743355280005945, + "docs/api/html/group__test__task__log__level__default.html": 11083738057915848718, + "venv/lib/python3.13/site-packages/cffi/model.py": 13544803366359102016, + "docs/api/html/group__FieldKind.html": 4665292646026475164, + "docs/api/html/group__TranslateRunRoutesModule.html": 10815514676447136360, + "docs/api/html/group__DashboardTaskHistoryResponse.html": 4494151273240950242, + "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js": 12559538850095902160, + "backend/src/services/auth_service.py": 12627214634675239140, + "venv/lib/python3.13/site-packages/PIL/PngImagePlugin.py": 5076219228736706283, + "venv/lib/python3.13/site-packages/pandas/tests/extension/date/array.py": 18073740473413382768, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filter.py": 16846063190466267618, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.pyi": 3783371827926657291, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/types.py": 13384984564922916167, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_api.py": 13319208721442328400, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_comparison.py": 4611207880323503098, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_utils.py": 15197339054699289627, + "docs/api/html/group____get__profile__service.html": 16201432222171400696, + "docs/api/html/group__truncate__text.html": 15964456244302237036, + "docs/api/html/bc_sd.png": 10097603501861382643, + "venv/lib/python3.13/site-packages/pydantic/_internal/_signature.py": 17075003201648027250, + "venv/lib/python3.13/site-packages/numpy/core/__init__.py": 10397294636564169377, + "venv/lib/python3.13/site-packages/pandas/core/apply.py": 17080038932032802984, + "docs/api/html/group__Permission.html": 12943669349918425540, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api2.pyx": 15936470837420356071, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_category.py": 3944898528119364040, + "venv/lib/python3.13/site-packages/PIL/ImageTransform.py": 7825940907094338808, + "docs/api/html/group__BackupRequest.html": 9361740148201115669, + "docs/api/html/group__test__features__from__env__still__applied.html": 5745023344665096858, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/__init__.py": 4761936067832638585, + "docs/api/html/group__startPolling__Function.html": 11240654189362337669, + "venv/lib/python3.13/site-packages/numpy/_typing/_extended_precision.py": 4201820494689784136, + "venv/lib/python3.13/site-packages/pandas/core/common.py": 14578909424716153057, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data": 11957232619107868023, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/datasource.pyi": 80441145804744162, + "venv/lib/python3.13/site-packages/fastapi/exception_handlers.py": 9907950997607808699, + "docs/api/html/group__log__requests.html": 1562379206737898620, + "docs/api/html/group__ValidationRunListResponse.html": 1652982914005412268, + "docs/api/html/group__DeploymentEnvironmentSchema.html": 3378454771345026670, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/core.py": 2199607970842512780, + "docs/api/html/group____dispatch__dataset__review__intent.html": 12800069212370219281, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi": 10784666243744187693, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo77.f": 16250175448122328648, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_info.py": 15151894356374015812, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_add_prefix_suffix.py": 4815518357936092237, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_indexing.py": 14461606056665485421, + "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.py": 11393954913234866059, + "backend/src/core/__tests__/test_throttled_scheduler.py": 3526995607557121417, + "docs/api/html/group__audit__repository.html": 4149687455544451812, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__main__.py": 6480933938145689964, + "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.pyi": 5269136485429780038, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_freq_attr.py": 7685756734926749093, + "docs/api/html/group__BulkReplaceModal.html": 1782439446991758677, + "backend/src/services/clean_release/repositories/policy_repository.py": 9074701224814573817, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_finalize.py": 12539815426478689538, + "docs/api/html/group__test__llm__analysis__timeout__constants.html": 8010871658971891725, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/inference.py": 2786441797589123900, + "docs/api/html/group____iter__route__files.html": 11857371736918099276, + "docs/api/html/group____make__config__manager.html": 7005648573888953729, + "frontend/build/_app/immutable/nodes/18.CucUwFHq.js": 76549391773507937, + "docs/api/html/group__list__gitea__repositories.html": 5900420781022521601, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/RECORD": 17545477863793260029, + "frontend/src/lib/components/reports/reportTypeProfiles.js": 10752964062056890725, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/styles/emacs.py": 4540417536507245024, + "docs/api/html/group__createAuthStore__Function.html": 15632886189609660791, + "venv/lib/python3.13/site-packages/pandas/core/ops/missing.py": 9493665944764255854, + "frontend/src/lib/components/translate/__tests__/test_translation_preview.svelte.js": 2673586123384011532, + "frontend/src/lib/api/__tests__/reports_api.test.js": 4759560641984508050, + "venv/lib/python3.13/site-packages/anyio/abc/_subprocesses.py": 4297459042599291531, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/__init__.py": 2397613800206300456, + "docs/api/html/group__TranslateDictionariesApi.html": 3796476465563433653, + "venv/lib/python3.13/site-packages/smmap/test/test_util.py": 5634593621291240094, + "venv/lib/python3.13/site-packages/git/objects/util.py": 691018806932355307, + "docs/api/html/group____get__default__environment__id.html": 17375226256254872481, + "venv/lib/python3.13/site-packages/numpy/random/_common.pxd": 3402284125943848851, + "venv/lib/python3.13/site-packages/pandas/util/_doctools.py": 3766479022075611396, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_hist_box_by.py": 16197297231683144299, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/request.py": 10484280068366411541, + "docs/api/html/group__TranslationE2E.html": 15552322666790950919, + "docs/api/html/group__ValidationReport.html": 2160384060642848740, + "docs/api/html/search/all_5.js": 15854593946678342554, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_datetime_index.py": 15448494560143263150, + "backend/alembic/versions/aa1b2c3d4e5f_drop_deprecated_translate_columns.py": 15433357059529111004, + "docs/api/html/group__test__load__tasks__with__limit.html": 16300281296351269896, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_s390_unix.h": 5161826643658505126, + "venv/lib/python3.13/site-packages/pandas/tests/test_multilevel.py": 10500316376521962061, + "docs/api/html/group__sendAssistantMessage__Function.html": 2736224334042274893, + "venv/lib/python3.13/site-packages/pip/_internal/cache.py": 210214424535803946, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/package_data.py": 3078502124870585867, + "venv/lib/python3.13/site-packages/charset_normalizer/md.py": 2029760700380476138, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x32_unix.h": 9165722966153119887, + "docs/api/html/group__fetchDatabases__Function.html": 8786717975484135660, + "docs/api/html/group__LaunchConfirmationPanel.html": 10912367734580070482, + "venv/lib/python3.13/site-packages/greenlet/tests/test_generator.py": 14156412320760444199, + "venv/lib/python3.13/site-packages/bcrypt/__init__.py": 11415155211917181362, + "venv/lib/python3.13/site-packages/authlib/oidc/registration/__init__.py": 9448552677419908109, + "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/test_array_with_attr.py": 6568496539994758985, + "docs/api/html/group__BackupsRedirectPage.html": 2891216165677443076, + "venv/bin/pip3.13": 13861749540792881808, + "docs/api/html/group__CleanReleaseCompliancePlugin.html": 18416685483683975434, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.cpython-313-x86_64-linux-gnu.so": 1211491156495529145, + "docs/api/html/group__main.html": 12159712531135979472, + "venv/lib/python3.13/site-packages/pygments/lexers/_vim_builtins.py": 6842381248727719900, + "docs/api/html/group__test__filters__existing__keys.html": 12141627945650887732, + "docs/api/html/group____handle__response__format__fallback.html": 11346867985412676592, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/WHEEL": 2357997949040430835, + "docs/api/html/group__toasts__module__Module.html": 5241674397829197436, + "docs/api/html/group__continue__merge.html": 1077939235555109294, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/RECORD": 1556051687830913019, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/exceptions.py": 11303331612406662162, + "docs/api/html/group__TranslationRun.html": 13758493070000589779, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_holiday.py": 8541197358003711845, + "docs/api/html/doxygen.svg": 12256108328721719854, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_reductions.py": 633644255158859565, + "docs/api/html/group__RunListResponse.html": 11273163658263807436, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/philox-testset-1.csv": 2202360898679162333, + "docs/api/html/group__add__validation__job.html": 8326497628811170515, + "docs/api/html/group__revoke__publication__endpoint.html": 828753596319821516, + "backend/src/services/clean_release/stages/internal_sources_only.py": 6150576100887638944, + "docs/api/html/group__generate__upsert__sql.html": 1809776775004922247, + "docs/api/html/group__AssistantApi.html": 466508613702940100, + "docs/api/html/group__TranslationJobConfig.html": 15629227135566862060, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_arithmetic.py": 16045758521544316139, + "docs/api/html/group__PolicyForm.html": 18395519007487395459, + "venv/lib/python3.13/site-packages/pygments/lexers/nit.py": 18262907536841158657, + "docs/api/html/group__create__branch.html": 8681906529509434557, + "dist/docker/ss-tools-repo.0.1.4.bundle": 12930375079272366930, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_cpp_exception.py": 11851283355840567373, + "venv/lib/python3.13/site-packages/pandas/tseries/__init__.py": 2648641118117738895, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunclike.pyi": 14623308681600207918, + "venv/lib/python3.13/site-packages/pygments/lexers/forth.py": 17623561228839319663, + "venv/lib/python3.13/site-packages/numpy/random/_sfc64.pyi": 3536657185495782642, + "docs/api/html/navtreeindex14.js": 14969246354997611461, + "docs/api/html/group__save__profile__and__findings.html": 2929839953576621200, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____endAllEvents.html": 3018938932417409550, + "docs/api/html/group__ProfileService.html": 6914047737105070955, + "docs/api/html/group__SessionDetail.html": 8883911469141750019, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_linux.h": 4071314837793906998, + "venv/lib/python3.13/site-packages/git/index/util.py": 5022401275553420722, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/git.py": 17060377178521307324, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-2.csv": 16611724551361692999, + "backend/src/api/routes/assistant/_tool_list_environments.py": 9497258201257484683, + "docs/api/html/group__load__schedules.html": 3794053930475684249, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/scalars.py": 3468820466230647963, + "backend/src/plugins/translate/orchestrator_config.py": 1227864697811313446, + "docs/api/html/group__SupersetContextParsingMixin____recover__dataset__binding__from__dashboard.html": 11941775473254198398, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/resolution.py": 5132658725516243311, + "docs/api/html/group__test__api__key__auth__revoked.html": 11241522851613980961, + "docs/api/html/group__CoreUtils.html": 3246108816835089471, + "venv/lib/python3.13/site-packages/pydantic/errors.py": 3506829204186508192, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_conversion_utils.py": 1567199588622767277, + "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe_protocol.py": 12857147443873820285, + "venv/lib/python3.13/site-packages/multipart/__init__.py": 6496114190873890851, + "venv/lib/python3.13/site-packages/git/objects/tree.py": 10239027037919781330, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_copy.py": 11335108864615756235, + "docs/api/html/group__DatasetReviewDependencies.html": 2190246894914856689, + "frontend/src/lib/components/layout/__tests__/test_breadcrumbs.svelte.js": 16890149311572703440, + "docs/api/html/group__SupersetClientBuildDatasetPreviewLegacyFormData.html": 16410404940636182538, + "venv/lib/python3.13/site-packages/psycopg2/sql.py": 18041240927130590808, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.pyi": 15498895254516573071, + "backend/src/services/clean_release/__tests__/test_preparation_service.py": 1135205964675811608, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_convert.py": 17957207974799052653, + "docs/api/html/group__PreviewRequest.html": 269103665065334891, + "docs/api/html/group__ensure__banner__chart.html": 16144864453932921383, + "docs/api/html/group__MaintenanceApi.html": 10684759386844628447, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_html.py": 12040484325988635032, + "docs/api/html/group__TranslationExecutionEngine.html": 6997454185504177526, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/__init__.py": 17630019785411546070, + "frontend/src/components/TaskRunner.svelte": 9781729605096065400, + "venv/lib/python3.13/site-packages/PIL/ImtImagePlugin.py": 17599267755504817169, + "venv/lib/python3.13/site-packages/numpy/testing/tests/test_utils.py": 5291132432383518704, + "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.pyi": 12043292763005874815, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/padding.py": 13763154154862871596, + "frontend/src/lib/stores/environmentContext.js": 9960741825828562980, + "frontend/src/pages/Dashboard.svelte": 16189908777731571070, + "docs/api/html/group__test__map__columns__success.html": 9453179995162588276, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/cache.py": 15037055977924167732, + "venv/lib/python3.13/site-packages/pandas/_libs/pandas_parser.cpython-313-x86_64-linux-gnu.so": 18056339336364727359, + "docs/api/html/group__test__migrate__dashboards__pre__checks.html": 3207110507188780911, + "docs/api/html/navtreeindex3.js": 4980194464875678268, + "venv/lib/python3.13/site-packages/PIL/McIdasImagePlugin.py": 14137545069906812251, + "docs/api/html/group__NoExternalEndpointsStage.html": 18436422189478912804, + "docs/api/html/group__test__reject__persists__decision__without__promoting__candidate__state.html": 7671132833499886533, + "venv/lib/python3.13/site-packages/passlib/handlers/scrypt.py": 10089372287597257402, + "docs/api/html/group__create__job.html": 8027737551110382669, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/version.py": 14723293207188512766, + "frontend/src/lib/components/layout/__tests__/test_taskDrawer.svelte.js": 7736587487210851851, + "docs/api/html/group__RepositoryRelations.html": 8149399210221155946, + "venv/lib/python3.13/site-packages/httpx/_transports/default.py": 8391302827983907910, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_resolution.py": 6076761538303762297, + "frontend/build/_app/immutable/chunks/Qyda_LTJ.js": 6932330203320525068, + "docs/api/html/group__DatasetsApiTests.html": 8138723528945535385, + "docs/api/html/group__frontend__src__lib__auth__store__ts____AuthState.html": 725779176116101299, + "docs/api/html/group__TestCoreScheduler.html": 8832629785112398533, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-1.csv": 5708101386018266998, + "docs/api/html/group__TranslateCorrectionRoutesModule.html": 12198261775802784372, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_mangle_dupes.py": 15613858733226279352, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_matrix_linalg.py": 2858987911347541177, + "frontend/build/_app/immutable/chunks/DdDuJAsf.js": 17661724911749600673, + "venv/lib/python3.13/site-packages/packaging/_elffile.py": 9546225990833172494, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadEvents.html": 2661201656392934757, + "backend/src/plugins/translate/_token_budget.py": 13508548690925198562, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/signals.py": 13677199634497673845, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/__init__.py": 15130871412783076140, + "docs/api/html/dynsections.js": 9903691534904054073, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/accessors.py": 12743680296510533030, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/scripts.py": 8759605303886441976, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_join.py": 17874591841487372992, + "frontend/playwright-report/trace/index.BCnMPevh.js": 8625484717968740943, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_builtin_md4.py": 16840174243259839994, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/common_with_division.f": 13517753924061311074, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__version.py": 9037916251087760255, + "venv/lib/python3.13/site-packages/_pytest/_io/saferepr.py": 8025234471485188730, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot.py": 334735766122314163, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/PIL/_imaging.pyi": 17119084207765761921, + "docs/api/html/group__test__commit__changes__applies__profile__identity__before__commit.html": 7275902928560381122, + "frontend/build/_app/immutable/chunks/B8i7aTDk.js": 3461841299904719399, + "venv/lib/python3.13/site-packages/pandas/core/arrays/floating.py": 1215586266958421991, + "docs/api/html/group__SupersetContextExtractorBase____decode__query__state.html": 5669940893460409072, + "docs/api/html/group__SupersetContextExtractorBase______init____.html": 1177619611547833454, + "docs/api/html/group__test__empty__key__cols__returns__all__rows.html": 72705144746688470, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_equals.py": 9903250467264848187, + "venv/lib/python3.13/site-packages/pip/_internal/models/index.py": 2839270935469791607, + "venv/lib/python3.13/site-packages/pandas/tests/config/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group____normalize__base__url.html": 5091254209090535827, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_datetimelike.py": 10294379314868108649, + "venv/lib/python3.13/site-packages/yaml/emitter.py": 6036657460416912844, + "docs/api/html/group__index__by__uuid.html": 3464688372161598541, + "docs/api/html/group__APIClient__authenticate.html": 3524465342974233149, + "docs/api/html/index.html": 6265284219880825892, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/wait.py": 6196757964348666082, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.cpython-313-x86_64-linux-gnu.so": 2311012065007769341, + "venv/lib/python3.13/site-packages/dateutil/parser/isoparser.py": 18015430204844361067, + "docs/api/html/group__test__no__prior__run__returns__all__rows.html": 3777590034380927800, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/conftest.py": 4124267277224879990, + "docs/api/html/group__Button.html": 10473089147190690338, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.cpython-313-x86_64-linux-gnu.so": 657138415665704488, + "docs/api/html/group__test__active__default__true.html": 1870632923808868585, + "backend/src/api/routes/__tests__/test_tasks_logs.py": 13245318244727271907, + "docs/api/html/group__BackupPlugin.html": 16158251941328976430, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/functions.py": 14107377669163193871, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_gen_files.py": 11149423183253618378, + "docs/api/html/group__AuthFixture.html": 17387223620144513588, + "docs/api/html/group__create__mapping.html": 1348589188703029518, + "docs/api/html/search/searchdata.js": 12335622014985822676, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.py": 15657897994221955882, + "venv/lib/python3.13/site-packages/pandas/io/sas/__init__.py": 8095743504390517992, + "venv/lib/python3.13/site-packages/greenlet/TMainGreenlet.cpp": 9489520001674089404, + "venv/lib/python3.13/site-packages/pandas/io/formats/xml.py": 12235904458551067063, + "venv/lib/python3.13/site-packages/pip/_internal/commands/completion.py": 4435231315212798769, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_any_index.py": 10800968419286329268, + "venv/lib/python3.13/site-packages/pygments/lexers/qvt.py": 1895735583818227972, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_sorted.py": 1899608688401238388, + "frontend/build/favicon.png": 16740551781088792605, + "docs/api/html/group__resume__task__with__password.html": 16159660988139123334, + "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiGet.html": 14001607880380347297, + "docs/api/html/group__TasksSessionLocal.html": 16634222622624891127, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming_py.py": 16133454512601432815, + "docs/api/html/group____resolve__or__create__conversation.html": 8613625745536476404, + "venv/lib/python3.13/site-packages/smmap/test/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/constructors.py": 7587476800569414769, + "venv/lib/python3.13/site-packages/yaml/cyaml.py": 9157893742065554693, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_types.py": 4083297168618981717, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/shared.py": 1162754506656505196, + "venv/lib/python3.13/site-packages/pandas/core/sparse/__init__.py": 15130871412783076140, + "docs/api/html/group____resolve__dashboard__id__entity.html": 6374255965303558769, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_tree.py": 15789443159423302291, + "docs/api/html/group__ExportArtifactResponse.html": 9908246940869758823, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_get_numeric_data.py": 5896318696417090902, + "venv/lib/python3.13/site-packages/uvicorn/workers.py": 7213290478118197047, + "docs/api/html/group__get__file__by__path.html": 8633346116618658558, + "venv/lib/python3.13/site-packages/urllib3/util/ssl_match_hostname.py": 7411350247004265632, + "venv/lib/python3.13/site-packages/certifi/core.py": 7472627735517449185, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_series.py": 16418555817529711079, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_fields.py": 16222559851336781264, + "venv/lib/python3.13/site-packages/pygments/lexers/pddl.py": 9256112535059995841, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pandas/core/nanops.py": 18291534943985310005, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_unique.py": 6234921572794573905, + "venv/lib/python3.13/site-packages/iniconfig/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.pyi": 8607205990821121708, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/markers.py": 8297667990263004097, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_map.py": 4761668276463940924, + "docs/api/html/group__test__load__schedules__loads__validation__policies.html": 9504795440748068750, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_dst.py": 12243642467695371088, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/modeline.py": 6194365452131561478, + "venv/lib/python3.13/site-packages/ecdsa/__init__.py": 6237633580090653788, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_head_tail.py": 1906121476322841736, + "docs/api/html/group__getUsers__Function.html": 16614665774395265088, + "backend/src/services/clean_release/repository.py": 2475446738311184132, + "docs/api/html/group__MetricDescriptionUpdate.html": 7274378175344562378, + "docs/api/html/group____coerce__intent__entities.html": 14588365769904720775, + "venv/lib/python3.13/site-packages/pandas/core/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/httpx/_transports/__init__.py": 5931944625631942279, + "venv/lib/python3.13/site-packages/pandas/io/stata.py": 14370692318835340323, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/conversion.pyi": 16976386782787920450, + "venv/lib/python3.13/site-packages/pluggy/_version.py": 7195758686717994475, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libk5crypto-b1f99d5c.so.3.1": 10669523137596317954, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/win64python2.npy": 11005857803794805939, + "docs/api/html/group__test__capabilities__question__returns__successful__help.html": 13279217848598726597, + "docs/api/html/group__MaintenanceSettings.html": 7078307981035215602, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval.py": 1228451182688768814, + "docs/api/html/group__BASE__DIR.html": 4143991800839240540, + "docs/api/html/group__startMigration__Function.html": 2057002933686227334, + "docs/api/html/group__compute__build__token__metadata.html": 12931039213712686625, + "docs/api/html/group__test__get__users__page__sends__lowercase__order__direction.html": 17648935033667456417, + "docs/api/html/group__FindingArea.html": 416719964302786471, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/RECORD": 6187512110158566509, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel_legacy.py": 12735901522735836481, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nep50_promotions.py": 6318578947214302182, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_function.py": 644452398244782315, + "frontend/src/lib/ui/Select.svelte": 1377129595022044035, + "backend/src/core/utils/superset_context_extractor/__init__.py": 16372223806424875537, + "backend/src/plugins/translate/preview_response_parser.py": 15690759478655959205, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_linalg.py": 2939739913218718132, + "venv/lib/python3.13/site-packages/pandas/core/indexes/frozen.py": 1419685664276640120, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/__init__.py": 15487893533770969571, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__TranslationPreviewLanguageResponse.html": 7881198517650898374, + "venv/lib/python3.13/site-packages/yaml/loader.py": 18284689412149643959, + "venv/lib/python3.13/site-packages/jose/backends/_asn1.py": 14123487576864105793, + "venv/lib/python3.13/site-packages/psycopg2/_ipaddress.py": 12332672835751787869, + "docs/api/html/group____json__load__if__needed.html": 17553294735423274387, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_rank.py": 5398520257852104723, + "backend/src/api/__init__.py": 5287505122015708040, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api_latest.c": 5472633890472031814, + "docs/api/html/group__TestReportsOpenapiConformance.html": 14837764071303153325, + "venv/lib/python3.13/site-packages/pygments/lexers/rita.py": 8105458520706587972, + "docs/api/html/group__FeaturesSettings.html": 5116404687015486795, + "docs/api/html/group__seed__trace__id.html": 9478009198218783393, + "venv/lib/python3.13/site-packages/pygments/lexers/text.py": 10572727755736597715, + "docs/api/html/group__test__execution__snapshot__skips__partial__imported__filters__without__values.html": 4065395894553398621, + "docs/api/html/globals_func_n.html": 12757886206585188105, + "venv/lib/python3.13/site-packages/pillow.libs/libpng16-4a38ea05.so.16.53.0": 13136089038268964667, + "backend/src/api/routes/translate/_preview_routes.py": 12316180738640292691, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/revocation.py": 16178499278312777474, + "venv/lib/python3.13/site-packages/jose/jws.py": 11592462993417182096, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_stmts.f90": 6123192245816905092, + "backend/src/services/git/_sync.py": 18140332203522670777, + "docs/api/html/group____resolve__repository__status.html": 14395426941616609866, + "docs/api/html/group____enforce__dictionary.html": 18173488447816081316, + "venv/lib/python3.13/site-packages/cryptography/x509/oid.py": 13909847799728532602, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_io.py": 4199750143323741326, + "venv/lib/python3.13/site-packages/numpy/f2py/__version__.py": 8588600299261398598, + "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.pyi": 6796322258258182775, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/bitwise_ops.pyi": 751490187478960891, + "backend/src/plugins/translate/orchestrator_runner.py": 6903994426268521649, + "docs/api/html/group__test__api__key__auth__environment__scope.html": 5049990905458759758, + "venv/lib/python3.13/site-packages/greenlet/TThreadState.hpp": 15599148954148720463, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pillow.libs/libwebp-d8b9687f.so.7.2.0": 116215608212382352, + "docs/api/html/group__SettingsRouter.html": 16627647098664476974, + "docs/api/html/group__is__masked__or__placeholder.html": 8401491085323739641, + "docs/api/html/group__SupersetClientCompileDatasetPreview.html": 15872086106011031941, + "docs/api/html/group__test__get__task__logs__success.html": 14138133046940925663, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/keyring/backends/Windows.py": 4103830951042463184, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/METADATA": 14158024963562115116, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_limited_api.py": 15788485008764560845, + "venv/lib/python3.13/site-packages/passlib/handlers/windows.py": 8969739861295277439, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/git/db.py": 6680123567509774609, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimes.py": 8863748366184419339, + "docs/api/html/group__GitServiceGitlabMixin.html": 362918173698022720, + "docs/api/html/group__cancelAssistantOperation__Function.html": 1438697842894474195, + "venv/lib/python3.13/site-packages/numpy/lib/npyio.py": 6782361985454523908, + "docs/api/html/group__get__dashboard__filter__binding.html": 2139882779492208369, + "venv/lib/python3.13/site-packages/numpy/_core/printoptions.py": 3897105663572772838, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_astype.py": 15275720655739710249, + "frontend/src/lib/ui/LanguageSwitcher.svelte": 15175441556916164874, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_categorical.py": 18254320571347851153, + "docs/api/html/group__test__text__only__model__returns__false.html": 3042385293654228545, + "venv/lib/python3.13/site-packages/referencing/tests/test_exceptions.py": 10846225989746274213, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_like.py": 2778905818573625118, + "venv/lib/python3.13/site-packages/authlib/oauth1/client.py": 7314311571224771, + "venv/lib/python3.13/site-packages/requests/adapters.py": 17161109043243436200, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libssl-81ffa89e.so.3": 13913673119166658781, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/authorization_server.py": 2913800420669417152, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/histograms.pyi": 9255620158831643201, + "docs/api/html/group__MappingsPageTemplate.html": 11838633745376383178, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_delete.py": 3485374671877063217, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/PIL/EpsImagePlugin.py": 1824020964179364491, + "venv/lib/python3.13/site-packages/git/__init__.py": 15867730798565725003, + "frontend/src/routes/datasets/review/ReviewWorkspaceRightRail.svelte": 13437011381072252917, + "venv/lib/python3.13/site-packages/numpy/lib/array_utils.py": 18135114588643176359, + "venv/lib/python3.13/site-packages/passlib/tests/__main__.py": 16477180140297957650, + "venv/lib/python3.13/site-packages/numpy/_array_api_info.py": 9709101823494959741, + "docs/api/html/search/groups_a.js": 10318961203603531169, + "docs/api/html/group__frontend__src__lib__stores__health__js____refresh.html": 8625269529469601783, + "docs/api/html/search/functions_2.js": 2385164718268290961, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__test__get__job__metrics.html": 1706423668878190645, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas7bdat.py": 8963983384208121526, + "venv/lib/python3.13/site-packages/yaml/parser.py": 18021832376874455801, + "venv/bin/dotenv": 953953349803507198, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/events.py": 15099787454570172797, + "docs/api/html/group__import__dictionary__entries.html": 15238522570085331527, + "backend/src/models/api_key.py": 1182309756601741825, + "venv/lib/python3.13/site-packages/httpx/_main.py": 1812303705973265093, + "docs/api/html/group__ValidationPolicyBase.html": 9049806250569046639, + "docs/api/html/group__TaskModels.html": 17614003799808664482, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_errstate.py": 10788459171326750780, + "docs/api/html/group__DashboardHelpers.html": 17415235275942195386, + "docs/api/html/group__test__single__event.html": 10966926393815748610, + "docs/api/html/group__build__execution__snapshot.html": 11905153660120769510, + "docs/api/html/menu.js": 10605519113858156037, + "docs/api/html/group__SupersetClientGetDatabasesSummary.html": 15095847088724585809, + "docs/api/html/group__MarkerLogger______init____.html": 16103251887807394758, + "venv/lib/python3.13/site-packages/pillow.libs/libbrotlidec-b57ddf63.so.1.2.0": 10032590058372320813, + "docs/api/html/group__get__dashboards.html": 12911221475273233338, + "docs/api/html/group__SourceIsolation.html": 15074253220325240781, + "docs/api/html/group__StoredFile.html": 17068629307196027308, + "venv/lib/python3.13/site-packages/psycopg2/tz.py": 9966680685796585198, + "docs/api/html/group__test__superset__client__import__dashboard__guard.html": 2274917772442192787, + "venv/lib/python3.13/site-packages/idna/package_data.py": 6583909243002417298, + "docs/api/html/group__LLMAnalysisService.html": 4482613675136608683, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/token.py": 11655733747403312358, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_function_base.py": 16624804022675533740, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_compression.py": 8061288329598682752, + "venv/lib/python3.13/site-packages/anyio/_core/_typedattr.py": 17584691045493791349, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_astype.py": 7542068949396266883, + "venv/lib/python3.13/site-packages/httpcore/_models.py": 2501866419898326733, + "backend/src/plugins/translate/preview.py": 16388414711962873105, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/__init__.py": 6489437464172324468, + "backend/src/api/routes/assistant/_dispatch.py": 9523334006338734493, + "backend/src/services/profile_utils.py": 7948249060696753446, + "venv/lib/python3.13/site-packages/smmap/util.py": 13276170835044840023, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.py": 1094797446508902066, + "docs/api/html/group__test__concurrent__run__skips.html": 8827273582130280115, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.h": 16138871162713175599, + "venv/lib/python3.13/site-packages/pip/_internal/utils/glibc.py": 12335359492756627313, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_api.py": 5813060676255093777, + "venv/lib/python3.13/site-packages/pandas/tests/test_aggregation.py": 16863055892192070549, + "venv/lib/python3.13/site-packages/pygments/lexers/jslt.py": 11805394130669644345, + "venv/lib/python3.13/site-packages/PIL/TgaImagePlugin.py": 9208942494595222259, + "venv/lib/python3.13/site-packages/pandas/_config/config.py": 1494288055210911451, + "backend/src/plugins/translate/__tests__/test_sql_generator.py": 7990788832878981127, + "docs/api/html/group__test__superset__client__module__loads.html": 18034638000182478147, + "docs/api/html/group__test__plugin__loader__imports__logger.html": 4750311694643540001, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraypad.py": 4907193085512342162, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_stride_tricks.py": 1064774375151677743, + "backend/src/services/clean_release/source_isolation.py": 9041802363608399236, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__ValidationPolicyResponse.html": 1043672337453196849, + "docs/api/html/group__AssistantMessageRequest.html": 8567828667244420847, + "docs/api/html/group__test__approve__rejects__blocked__report.html": 4677486143136601773, + "docs/api/html/group__PageHeader.html": 5497869529552626290, + "docs/api/html/group__startDryRun__Function.html": 1798526875933461819, + "docs/api/html/navtreeindex4.js": 237051005493385994, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/getlimits.pyi": 93747984191461691, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/hooks.py": 8580161128344266243, + "docs/api/html/group__TestGitApi.html": 384988964900518925, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ciphers.pyi": 18143251582901996072, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_asfreq.py": 6162659555626011770, + "docs/api/html/globals_func_r.html": 4828713620959168383, + "venv/lib/python3.13/site-packages/_pytest/helpconfig.py": 3033477806668470951, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_constructors.py": 2786493777737472376, + "venv/lib/python3.13/site-packages/pandas/_libs/properties.cpython-313-x86_64-linux-gnu.so": 7092136701092358552, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dtypes.py": 4985057130807802395, + "docs/api/html/group__LLMResponseParser.html": 18031787096044719689, + "docs/api/html/group__test__us3__preview__response__propagates__refreshed__session__version__for__launch__follow__up.html": 14305667278559815270, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_m68k_gcc.h": 6580173800099450404, + "venv/lib/python3.13/site-packages/pip/_internal/__init__.py": 7060345458485901709, + "frontend/src/routes/git/+page.svelte": 3351712750068549963, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/__init__.py": 8705044848308260480, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_ddl.py": 3690189094298044728, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/isocintrin/isoCtests.f90": 6208802899442270399, + "docs/api/html/group__test__execution__snapshot__preserves__mapped__template__variables__and__filter__context.html": 9629082711066592094, + "venv/lib/python3.13/site-packages/httpcore/_async/__init__.py": 491192949420297153, + "venv/lib/python3.13/site-packages/_pytest/_io/__init__.py": 1510478951261938348, + "venv/lib/python3.13/site-packages/pygments/styles/stata_dark.py": 324072437809944751, + "venv/lib/python3.13/site-packages/pandas/util/_test_decorators.py": 17930801441803090445, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/dax.py": 10868881057512002309, + "venv/bin/pytest": 2593802493707545818, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/inout.f90": 1521999519583206538, + "docs/api/html/group__ClarificationHelpers.html": 2159036796456300532, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_ufunc.py": 3326502989311831856, + "frontend/src/components/storage/FileUpload.svelte": 14890211974307083938, + "venv/lib/python3.13/site-packages/sqlalchemy/events.py": 9329355486544822483, + "docs/api/html/group__test__virtual__dataset__match.html": 2595415352137646256, + "venv/lib/python3.13/site-packages/httpcore/_backends/auto.py": 10488908050307887802, + "docs/api/html/search/all_10.js": 7221993751774974193, + "docs/api/html/group__CompiledSQLPreview.html": 3548673488714120828, + "venv/lib/python3.13/site-packages/pygments/lexers/openscad.py": 13045825115225232396, + "venv/lib/python3.13/site-packages/packaging/tags.py": 1999137344425005827, + "docs/api/html/group__test__checking__state.html": 1723457618410256625, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_clearing_run_switches.py": 3020294789560975101, + "docs/api/html/group__DatasetReviewFilterModels.html": 3609237248730765067, + "docs/api/html/group____is__dashboard__endpoint.html": 16400823815894598847, + "venv/lib/python3.13/site-packages/cffi/pkgconfig.py": 16057517851163939571, + "docs/api/html/group__DashboardModels.html": 15051964313793389785, + "docs/api/html/group__LogEntry.html": 3138617379009952451, + "docs/api/html/group__test__get__datasets__superset__failure.html": 14758789957406942338, + "venv/lib/python3.13/site-packages/gitdb/test/test_stream.py": 18159319442241857857, + "docs/api/html/group__test__repository__typo__fixed.html": 4874005037242233206, + "docs/api/html/group__DatasetReviewSessionRepository.html": 6205499016168304119, + "venv/lib/python3.13/site-packages/pillow.libs/liblzma-61b1002e.so.5.8.2": 11405063484938458842, + "docs/api/html/group__MaintenanceEndResponse.html": 11532173656825010057, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_downcast.py": 10399746645741452200, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked_shared.py": 8453174300922339015, + "frontend/src/routes/dashboards/[id]/components/DashboardTaskHistory.svelte": 6764180689056946702, + "docs/api/html/group__delete__repository.html": 16831781850407059770, + "venv/lib/python3.13/site-packages/jeepney/bus.py": 5163075760432139100, + "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.pyi": 2042169688753984918, + "venv/lib/python3.13/site-packages/pyasn1/type/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_des.py": 7198931807116651263, + "venv/lib/python3.13/site-packages/pygments/lexers/ampl.py": 4594260378653369985, + "docs/api/html/search/groups_11.js": 14985601485741813433, + "venv/lib/python3.13/site-packages/rsa/prime.py": 10820898885044917095, + "docs/api/html/group__test__validation__policy__create__accepts__structured__custom__channels.html": 12324191215161408510, + "docs/api/html/group__test__get__repository__diff__propagates__http__exception.html": 8311817278452456051, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/jupyter.py": 4446535289156468239, + "docs/api/html/group__runTask__Function.html": 7728857557116934185, + "docs/api/html/group__StorageIndexPage.html": 3670377713830788417, + "venv/lib/python3.13/site-packages/anyio/_backends/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_exceptions.py": 15290185665769490169, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.py": 5221550291012182443, + "docs/api/html/group__TestProfileApi.html": 12173172201557312976, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log1p.csv": 16333455295913164408, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/__init__.py": 16150188643076000414, + "docs/api/html/group__SupersetContextExtractorBase____search__nested__numeric__key.html": 8181622518576542399, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/kind/foo.f90": 1367964042730902899, + "docs/api/html/group__build__manifest__snapshot.html": 4326848185717081568, + "venv/lib/python3.13/site-packages/pygments/__init__.py": 14493583771211110135, + "venv/lib/python3.13/site-packages/pydantic/_internal/_internal_dataclass.py": 13206778611516171760, + "frontend/build/_app/immutable/nodes/39.IvmujnrG.js": 7634837887095258352, + "docs/api/html/group__run__manifest__build.html": 9207320813511287781, + "venv/lib/python3.13/site-packages/rsa/randnum.py": 13958355153634534526, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpcre-9513aab5.so.1.2.0": 12298861477332583668, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsin.csv": 1665171675551692225, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/setup.py": 17086801293303202021, + "docs/api/html/group__frontend__src__lib__stores__sidebar__js____setMobileOpen.html": 14669790376571342620, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_mask.py": 5451866071307236899, + "docs/api/html/group____recovery__imports.html": 14364048882219717740, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_getitem.py": 8414677531950073373, + "docs/api/html/group____latest__publication__for__candidate.html": 3876828569531437570, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsinh.csv": 13695545125848146013, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_analytics.py": 4510712334676848202, + "docs/api/html/resize.js": 16477870345255205251, + "docs/api/html/group__ValidationStatus.html": 3516721738710393355, + "docs/api/html/group__resolveStorageQueryFromPath__Function.html": 13600414603292373532, + "venv/lib/python3.13/site-packages/pygments/lexers/nimrod.py": 8457135214131458774, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/groupby.py": 12749730954888633240, + "frontend/build/_app/immutable/chunks/Csip6vSb.js": 14038946045947560490, + "backend/git_repos/remote/test-repo.git/refs/heads/feature/e2e-update": 9577764946931187657, + "docs/api/html/group__get__all__tasks.html": 11728882406952380263, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937_regressions.py": 8981142329261965538, + "docs/api/html/group__frontend__src__lib__api__validation__js____createTask.html": 11277710494667674474, + "venv/lib/python3.13/site-packages/pydantic/networks.py": 15767095796055034509, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/WHEEL": 7127684561765977531, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_chained_assignment_deprecation.py": 7694137945143129474, + "docs/api/html/group__test__trigger__validation__skips__inactive__policy.html": 10751741691328808103, + "backend/src/api/routes/git/_merge_routes.py": 4402800010188126280, + "venv/lib/python3.13/site-packages/pandas/core/arrays/masked.py": 13977195666947626206, + "docs/api/html/group__test__resolve__from__dictionary__prefers__exact__match.html": 11830592332306970257, + "venv/lib/python3.13/site-packages/secretstorage/item.py": 15486045386471912693, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA_py.py": 2494967094400350388, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_mod.f90": 5765181406654284736, + "docs/api/html/group__LogLevel.html": 10025057817499119875, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_groupby_shift_diff.py": 1210657479716368827, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz": 17626265752554406355, + "docs/api/html/group__test__multiple__stale__pending__all__cleared.html": 9272463816474981933, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/__init__.py": 16123561007855424016, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_interpolate.py": 6154072863387383113, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/poolmanager.py": 5071697837617938064, + "docs/api/html/group__update__dictionary.html": 9189704466838089002, + "venv/lib/python3.13/site-packages/httpcore/_backends/mock.py": 6849131425727092784, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_boxplot_method.py": 7635131059308651529, + "backend/tests/test_api_key_auth.py": 2617154971155928462, + "docs/api/html/group__test__register__candidate__rejects__malformed__artifact__input.html": 7952314594070523566, + "docs/api/html/group__TranslationOrchestrator.html": 6470000776760688783, + "docs/api/html/group__RiskAssessorModule.html": 17900668792000979877, + "docs/api/html/group__get__user__by__username.html": 2246686587123622263, + "venv/lib/python3.13/site-packages/gitdb/__init__.py": 15381521749253695923, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_interval.py": 2613503970235591726, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append.py": 13152738905565588661, + "backend/src/core/scheduler.py": 8479779689131957613, + "backend/src/plugins/translate/orchestrator_retry.py": 2531207787915962557, + "docs/api/html/group__ValidationPolicyCreate.html": 15832204788479864944, + "docs/api/html/group__CandidateStatus.html": 1355862946696425686, + "docs/api/html/group____get__owned__mapping__or__404.html": 17632372297671515109, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending.pyx": 756189645898997788, + "venv/lib/python3.13/site-packages/websockets/extensions/permessage_deflate.py": 12957105912018691390, + "venv/lib/python3.13/site-packages/pydantic/_internal/_core_utils.py": 13248004771466837645, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_interval_array_equal.py": 15654625365223535344, + "backend/src/services/git/_status.py": 13381104725964137243, + "docs/api/html/group____build__banner__text__for__dashboard.html": 5689918064001964476, + "docs/api/html/group__build__candidate__manifest__v2__endpoint.html": 1880625190919477701, + "backend/src/core/async_superset_client.py": 339440171038902146, + "docs/api/html/group__create__provider.html": 15799595578960944885, + "venv/lib/python3.13/site-packages/smmap/test/test_mman.py": 8977119225749253844, + "venv/lib/python3.13/site-packages/PIL/_imagingmath.pyi": 18222325750818585549, + "docs/api/html/group__docker__backend__entrypoint__install__certificates.html": 18122978905545820697, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_hashing.py": 11119284926039008503, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh2848.f90": 5799106109999071412, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pluggy/_tracing.py": 7276210024011809655, + "docs/api/html/group__RunHistoryFilter.html": 14147174199498703539, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/printing.py": 8681519649669408795, + "venv/lib/python3.13/site-packages/pydantic_settings/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/parse.py": 2427019397934437210, + "venv/lib/python3.13/site-packages/pygments/lexers/textfmts.py": 17438453283146671997, + "frontend/src/lib/stores/__tests__/mocks/environment.js": 3065694655373335793, + "docs/api/html/group__app__middleware.html": 1146307130445661, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py": 15928258358116399067, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/licenses/LICENSE": 1424673738228762254, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename.py": 4890779493250245903, + "backend/src/core/ws_log_handler.py": 10832938920665412398, + "venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py": 932468678264370661, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py": 13869355993837330406, + "docs/api/html/group__test__get__dialect__from__database__unsupported.html": 1556786983214372955, + "frontend/src/lib/components/layout/__tests__/test_sidebar.svelte.js": 15166117912533507471, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/code.py": 9700811903057180696, + "docs/api/html/group__with__source.html": 10190434660091889570, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string.py": 3834067244989779682, + "docs/api/html/group__test__cli__candidate__register__scaffold.html": 18082978099842834637, + "docs/api/html/group__TestMigrationEngine.html": 2873879812846856614, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/authorization_server.py": 17967809814827547139, + "venv/lib/python3.13/site-packages/passlib/handlers/scram.py": 12281469744083867450, + "docs/api/html/group__send__message.html": 13998342804671032700, + "docs/api/html/group__TestDictionaryCRUD.html": 909187372871752623, + "docs/api/html/group__apply__record__action.html": 5054356330772600177, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_monotonic.py": 4626065848596214108, + "docs/api/html/group__ExecutionMappingDto.html": 13993327359271368811, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/parameters.py": 1647490964392525933, + "frontend/tailwind.config.js": 13139381085024634801, + "docs/api/html/group__test__get__repository__status__batch__returns__mixed__statuses.html": 7606080893793687994, + "docs/api/html/group__AssistantClarificationCard.html": 6642464260066671228, + "venv/lib/python3.13/site-packages/greenlet/platform/__init__.py": 15130871412783076140, + "docs/api/html/group__llm__report__screenshot__effect__contract__Function.html": 11766872533400793722, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_join.py": 13115782559805895662, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_isfile.py": 14544060093268573418, + "venv/lib/python3.13/site-packages/pydantic/_internal/_repr.py": 1758478941817868344, + "venv/lib/python3.13/site-packages/pandas/tests/window/conftest.py": 10673264126684571493, + "docs/api/html/group__get__schedule.html": 11620587396014813935, + "venv/lib/python3.13/site-packages/pandas/io/json/_normalize.py": 15938855324762935595, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_hour.py": 14128508606361694814, + "venv/lib/python3.13/site-packages/numpy/core/__init__.pyi": 15130871412783076140, + "docs/api/html/group__DashboardTypes__Module.html": 17788793500352327911, + "venv/lib/python3.13/site-packages/pydantic/annotated_handlers.py": 10186120300163345360, + "docs/api/html/group____execute__insert__sql.html": 14665396960311782890, + "frontend/src/routes/translate/[id]/+page.svelte": 8340407100798106676, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_inference.py": 5741637509343419942, + "docs/api/html/group__test__git__plugin__uses__shared__config__manager.html": 7129798876556226875, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libsasl2-883649fd.so.3.0.0": 13306917506408577991, + "venv/lib/python3.13/site-packages/apscheduler/triggers/calendarinterval.py": 12481650996882675949, + "venv/lib/python3.13/site-packages/pygments/lexers/shell.py": 3842338968014057744, + "docs/api/html/group____load__fixture.html": 803262356442504240, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/WHEEL": 6514509858522675228, + "frontend/build/_app/immutable/nodes/5.CzaflPnD.js": 3080600570375085864, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_constructors.py": 6515602441449955627, + "venv/lib/python3.13/site-packages/pydantic/datetime_parse.py": 196119761382305574, + "docs/api/html/group__suggest__mappings.html": 12832332337511809353, + "venv/lib/python3.13/site-packages/pandas/conftest.py": 747626550806435763, + "docs/api/html/group__subscribe__dataset__events.html": 12095461130299345778, + "venv/lib/python3.13/site-packages/pygments/lexers/archetype.py": 8995143180482517233, + "venv/lib/python3.13/site-packages/pygments/lexers/ml.py": 7510453371068494792, + "docs/api/html/group__test__manifest__count__mismatch.html": 16626531910432296321, + "docs/api/html/group__resolve__future.html": 11803620953629948836, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_partial_slicing.py": 15567512240469921599, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_has_cy.py": 11979732417254560424, + "backend/src/core/auth/__init__.py": 16435384828055133073, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/RECORD": 3288669175908898843, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/tags.py": 254599446317461188, + "docs/api/html/group__duplicateTask__Function.html": 6572424327379812707, + "venv/lib/python3.13/site-packages/numpy/core/overrides.py": 11228841530565061867, + "venv/lib/python3.13/site-packages/passlib/tests/sample_config_1s.cfg": 1809713544666311828, + "docs/api/html/group__ResourceMapping.html": 12067212261017894140, + "docs/api/html/group____dispatch__intent.html": 9089068514794356054, + "docs/api/html/group__promote__dashboard.html": 8826303047033817087, + "frontend/src/components/git/ConflictResolver.svelte": 12314421232300049275, + "frontend/src/components/git/GitInitPanel.svelte": 12876611910996416326, + "venv/lib/python3.13/site-packages/git/refs/__init__.py": 16246686106073400519, + "docs/api/html/group__GitServerConfigCreate.html": 6897428372780872078, + "docs/api/html/group__test__parse__datetime__datetime__object.html": 6514218695236425243, + "docs/api/html/group__frontend__src__lib__api__validation__js____fetchTasks.html": 2813676835730064546, + "docs/api/html/group__wait__for__input.html": 11071282356984674501, + "venv/lib/python3.13/site-packages/pip/_internal/req/constructors.py": 17485271035479378662, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/RECORD": 11976403473353722969, + "docs/api/html/group__SupersetCompilationAdapter____request__superset__preview.html": 13296042812426892350, + "docs/api/html/group__TermCorrectionSubmit.html": 15931221897139852522, + "docs/api/html/group__build__impact__summary.html": 17415672638957538101, + "docs/api/html/group__ValidationPolicy.html": 17809048165198520903, + "venv/lib/python3.13/site-packages/pandas/core/algorithms.py": 487939626203097713, + "venv/lib/python3.13/site-packages/pygments/styles/rrt.py": 9490973359165080360, + "docs/api/html/group__TaskLog.html": 12777742835079365241, + "docs/api/html/group__test__button__check__disabled__logic.html": 3976064615352650231, + "venv/lib/python3.13/site-packages/pluggy/py.typed": 15130871412783076140, + "docs/api/html/group__CheckStageResult.html": 1868186322282636304, + "docs/api/html/group__Environment.html": 9167040397257623124, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/claims.py": 14794441479740527265, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/jeepney/tests/test_fds.py": 6257376331171761928, + "venv/lib/python3.13/site-packages/gitdb/db/pack.py": 13460574575424209609, + "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.py": 15989335816452913951, + "docs/api/html/group__LlmSettings.html": 17767977673670132111, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/models.py": 3567900447243180991, + "venv/lib/python3.13/site-packages/click/_termui_impl.py": 3296085065310341855, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_replace.py": 8165088653532047247, + "docs/api/html/group__TestDictionaryUtils.html": 2864150937588414580, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arraymethod.py": 14421188237635557797, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/highway/LICENSE": 15636145745140922094, + "venv/lib/python3.13/site-packages/charset_normalizer/md.cpython-313-x86_64-linux-gnu.so": 1835299664481804968, + "backend/src/scripts/clean_release_tui.py": 3006670970245009659, + "backend/src/plugins/translate/service_bulk_replace.py": 5355744069308185607, + "docs/api/html/group__GlobalSetup.html": 2598937001273501380, + "docs/api/html/group__NetworkError______init____.html": 13550401008734324366, + "docs/api/html/group__get__dashboard__permalink__state__async.html": 14127921399924019544, + "docs/api/html/group__MockMappingService.html": 3005546302419221307, + "venv/lib/python3.13/site-packages/starlette/_utils.py": 8384087194580626069, + "docs/api/html/search/groups_7.js": 14622460438011001410, + "docs/api/html/group__WebSocketLogHandler.html": 930622065954511642, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_show_versions.py": 10577134135855180356, + "venv/lib/python3.13/site-packages/pygments/lexers/jvm.py": 897669925284011089, + "frontend/src/lib/components/dataset-review/LaunchConfirmationPanel.svelte": 16168905765241471813, + "venv/lib/python3.13/site-packages/pandas/tests/util/conftest.py": 11143177766735481660, + "venv/lib/python3.13/site-packages/fastapi/cli.py": 13972351579061154035, + "venv/lib/python3.13/site-packages/greenlet/PyModule.cpp": 3464405102256525511, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.py": 3654583356259850174, + "venv/bin/f2py": 11006035631504708368, + "docs/api/html/group__MappingMethod.html": 7292386745422091497, + "venv/lib/python3.13/site-packages/starlette/status.py": 16192064443996706569, + "docs/api/html/group__test__approve__rejects__duplicate__approve.html": 11531613578978927773, + "venv/lib/python3.13/site-packages/gitdb/stream.py": 3035738824550231197, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polyutils.py": 675639791972685455, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reflection.py": 1799734694597531652, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/METADATA": 9458019064618635438, + "frontend/build/_app/immutable/chunks/FoNxFoDF.js": 9853409041327023431, + "backend/src/core/database.py": 15117354038948355632, + "docs/api/html/group__build__detector.html": 17019644481515088584, + "docs/api/html/group__test__list__runs__success.html": 10465705390005371941, + "venv/lib/python3.13/site-packages/fastapi/middleware/cors.py": 7452860773712601508, + "docs/api/html/group__SemanticLayerReview.html": 10939641694027258903, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/strategies.py": 14007486769065345085, + "docs/api/html/group__SupersetUserProjection.html": 4661710962417506510, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/METADATA": 3643456577199395707, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/type_check.pyi": 9177256991985898824, + "docs/api/html/globals_e.html": 5635107513608756533, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____updateTranslationRunState.html": 9070808497640938645, + "venv/lib/python3.13/site-packages/pydantic/fields.py": 4142211199816898047, + "docs/api/html/group__get__user__dashboard__preference.html": 17414742887363265789, + "docs/api/html/group__test__persist__tasks__batch.html": 10046649763450222486, + "venv/lib/python3.13/site-packages/websockets/streams.py": 15786581670248099415, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_counting.py": 5628494712451120258, + "venv/lib/python3.13/site-packages/PIL/ImageOps.py": 3884310863548175682, "docs/api/html/group__AssistantAction.html": 12554190230328062237, + "docs/api/html/group__test__list__runs__filters.html": 15556674326842168757, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_multi_thread.py": 5660352470978621226, + "docs/api/html/group__test__get__datasets__env__not__found.html": 15409672532193793329, + "venv/lib/python3.13/site-packages/cryptography/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-2.csv": 5423145402839576695, + "venv/lib/python3.13/site-packages/PIL/PdfParser.py": 13710342183037972042, + "venv/lib/python3.13/site-packages/pygments/lexers/meson.py": 14659991367778746658, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_replace.py": 4953527309376861139, + "frontend/src/components/llm/DocPreview.svelte": 14349224708133169902, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/errors.py": 12777931960088171310, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_dialect.py": 6294522482440261756, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/apps.py": 4219758952914498116, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/warnings_and_errors.pyi": 3151453262733619734, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asof.py": 17616601748321325187, + "venv/lib/python3.13/site-packages/pycparser/__init__.py": 16499940957411260458, + "venv/lib/python3.13/site-packages/pip/_internal/models/scheme.py": 4101322315594430874, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/conftest.py": 17897502508251413988, + "docs/api/html/group__test__delete__logs__for__task.html": 15106642576678649464, + "docs/api/html/group__create__temp__file.html": 13523091680298755029, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____resolveEnvStage.html": 14282337486295968298, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/block_docstring/foo.f": 2489756567993745076, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24008.f": 3872245077994936272, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_reductions.py": 3575617237516305456, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/WHEEL": 8600534672961461758, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_arrow_interface.py": 9757086292358759236, + "docs/api/html/group__test__parse__datetime__invalid__string.html": 3666486688843083256, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/tests/test_reloading.py": 17532116685329953400, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_cross.py": 17742712348672218852, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/hstore.py": 9700253004947691961, + "docs/api/html/group__TextCleaner.html": 4066249505895592095, + "venv/lib/python3.13/site-packages/numpy/lib/_version.py": 15294708923334674060, + "backend/test_app.db": 2486636812009447051, + "venv/lib/python3.13/site-packages/pydantic_settings/__init__.py": 4374954204580208673, + "docs/api/html/group__test__create__remote__repository__creates__provider__repo.html": 13400861927477164469, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/basereload.py": 14479857245734482609, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/rule.py": 5318344538761246105, + "venv/lib/python3.13/site-packages/pydantic/_internal/_validators.py": 2939255306814626438, + "venv/lib/python3.13/site-packages/pygments/lexers/haxe.py": 1915270891525929751, + "docs/api/html/group____audit.html": 6003328311178572551, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/statreload.py": 5018382301336802445, + "venv/lib/python3.13/site-packages/passlib/utils/compat/_ordered_dict.py": 14248790502300368024, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__SourceRegistrySnapshot.html": 16441767339963706656, + "docs/api/html/group__test__list__banners.html": 725331980400835161, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_getitem.py": 18202683049446557047, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_constructors.py": 9478848417896027917, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x448.py": 3960540415600165213, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/base.py": 6348888156686452825, + "frontend/src/lib/components/translate/TermCorrectionPopup.svelte": 2290415616531498089, + "docs/api/html/group__EnvironmentMock__Module.html": 14116359480209547116, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/argon2.py": 15389474901714096890, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py": 11225173272050906977, + "backend/logs/app.log.1": 18386611762182777748, + "venv/lib/python3.13/site-packages/pydantic/_internal/_forward_ref.py": 10623110912945011703, + "venv/lib/python3.13/site-packages/urllib3/py.typed": 12220177918227495093, + "docker/frontend.entrypoint.sh": 11397342350356400663, + "venv/lib/python3.13/site-packages/pip/_vendor/typing_extensions.py": 10158933506062403688, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE": 13479831069780783137, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/METADATA": 15225661995655327539, + "frontend/e2e/tests/settings.e2e.js": 1420830886812189435, + "docs/api/html/group____build__insert__rows.html": 14245761913362401637, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/simple.py": 11050636051019777317, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/quoted_character/foo.f": 15318083678609435813, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_index_as_string.py": 13421551616136437100, + "docs/api/html/group__disable__schedule.html": 12452994962801576996, + "docs/api/html/group__TranslateHistoryPage.html": 13167035080416259682, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_monotonic.py": 14648437502004398715, + "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.pyi": 4587511199710546030, + "docs/api/html/group__TranslationScheduler.html": 13712731753540269518, + "venv/lib/python3.13/site-packages/httpcore/_sync/socks_proxy.py": 4418971149260853240, + "frontend/src/routes/datasets/review/[id]/+page.svelte": 6049610035268553847, + "docs/api/html/group__ReactivePasswordPrompt__Block.html": 6497383366601912081, + "backend/src/api/routes/git/_deps.py": 11478430655620147674, + "docs/api/html/group__test__report__validation.html": 17827285092454190901, + "venv/lib/python3.13/site-packages/pip/_internal/models/__init__.py": 13482731985065116750, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py": 9559295461619813890, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/assertion_client.py": 4261295109880943573, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dsa.py": 8079219938116542284, + "venv/lib/python3.13/site-packages/starlette/middleware/base.py": 16850428285382256618, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/licenses/LICENSE": 12688029424398428498, + "docs/api/html/group__LayoutUtils.html": 6665402780325047543, + "docs/api/html/group__test__start__missing__tables.html": 12776370102130311528, + "docs/api/html/group__test__persist__task__update.html": 8785260065895714433, + "docs/api/html/group__test__ensure__encryption__key__generates__backend__env__file.html": 17024487701570331364, + "docs/api/html/group____build__documentation__export.html": 16699691799165712675, + "venv/lib/python3.13/site-packages/passlib/tests/__init__.py": 16543763165305017760, + "backend/src/core/task_manager/task_logger.py": 6134165406387688582, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/cells.py": 8299309127027168194, + "venv/lib/python3.13/site-packages/pygments/formatters/terminal256.py": 11473927781167132440, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/WHEEL": 8600534672961461758, + "docs/api/html/group__SMTPProvider.html": 4088022190794089412, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_pickle.py": 8705060420848529055, + "venv/lib/python3.13/site-packages/jeepney/io/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/PIL/_imagingft.cpython-313-x86_64-linux-gnu.so": 11054435406031360190, + "docs/api/html/group__get__db.html": 16144093670776149736, + "docs/api/html/group____make__add__log__callback.html": 1369040010068771538, + "docs/api/html/group__ProfilePreferenceUpdateRequest.html": 4083205289293169459, + "venv/lib/python3.13/site-packages/pydantic/v1/tools.py": 1995883197897112278, + "backend/src/services/dataset_review/semantic_resolver.py": 11369306880421964134, + "venv/lib/python3.13/site-packages/PIL/_webp.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_date_range.py": 16128552513324017349, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/row.py": 9763935103055608874, + "docs/api/html/group__BreadcrumbsContractTest__Module.html": 11002777887735789654, + "venv/lib/python3.13/site-packages/passlib/tests/sample1c.cfg": 13088198405762342506, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_concatenate_chunks.py": 9693420220003544490, + "docs/api/html/group__create__dictionary.html": 15927500895860773676, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_interp_fillna.py": 5530041283991655415, + "venv/lib/python3.13/site-packages/pygments/lexers/csound.py": 2175454840368374515, + "venv/lib/python3.13/site-packages/pydantic/v1/errors.py": 11245135449608452720, + "venv/lib/python3.13/site-packages/referencing/retrieval.py": 11921179805977833120, + "venv/lib/python3.13/site-packages/pygments/lexers/sieve.py": 15757825442941567629, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/tree.py": 4991806303236874829, + "venv/lib/python3.13/site-packages/pandas/core/reshape/pivot.py": 4736553530021653868, + "docs/api/html/group__test__belief__scope__success__coherence.html": 9904691934239125152, + "docs/api/html/group__test__get__dashboard__thumbnail__202.html": 9047480873947971115, + "frontend/src/lib/toasts.js": 2743447773797450227, + "venv/lib/python3.13/site-packages/pyasn1/codec/der/encoder.py": 4790267438222037867, + "venv/lib/python3.13/site-packages/passlib/ext/django/models.py": 12386223748379294996, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_factorize.py": 5585211699185937865, + "venv/lib/python3.13/site-packages/starlette/middleware/gzip.py": 4321371046149637632, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__test__parse__datetime__none.html": 16098810779566927434, + "docs/api/html/group__EventBus.html": 1131138632704451045, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/AUTHORS": 10073225185964928425, + "docs/api/html/group__superset__lookup__service.html": 13295169492630932341, + "docs/api/html/group__TaskDrawerStore.html": 6675046395365943586, + "venv/lib/python3.13/site-packages/numpy.libs/libgfortran-040039e1-0352e75f.so.5.0.0": 13946067868274850557, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hi77.f": 10617763407280454334, + "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.pyi": 17825266810219495672, + "docs/api/html/group__test__parse__dashboard__url__for__filters__native__filters__direct.html": 15481616716847220529, + "venv/lib/python3.13/site-packages/pygments/lexers/kuin.py": 2818054539831699366, + "frontend/build/_app/immutable/chunks/Ccet3Ghj.js": 17230154466470532900, + "docs/api/html/group__test__git__service__get__repo__path__guard.html": 14019768017372580436, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_unix.h": 9608771997528633681, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/__init__.py": 371829636800613137, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/foo.f": 6349567809926050379, + "docs/api/html/group__build__stage__run__record.html": 374732547729464177, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/py.typed": 9796674040111366709, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/recarray_from_file.fits": 15116318600514274152, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_indexing.py": 3841182456456779012, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/ecdsa/test_jacobi.py": 1249543655136186433, + "venv/lib/python3.13/site-packages/rapidfuzz/process.pyi": 6959881293311340830, + "docs/api/html/group__mapping__service.html": 1146234231481143720, + "docs/api/html/group__TranslateJobCreate.html": 13885891459376922086, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_interpolate.py": 7354685880215444946, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildWorkspaceLaunchBlockers.html": 15003605714654482517, + "venv/lib/python3.13/site-packages/pygments/lexers/ada.py": 17032408976634308999, + "docs/api/html/group__test__compile__dataset__preview__prefers__legacy__explore__form__data__strategy.html": 15164673100544799582, + "docs/api/html/group__ResourceService.html": 11914817317990021619, + "venv/lib/python3.13/site-packages/pygments/lexers/automation.py": 8021926838024836904, + "backend/src/services/clean_release/__tests__/test_source_isolation.py": 462142504664192849, + "venv/lib/python3.13/site-packages/keyring/completion.py": 1027436016070040568, + "venv/lib/python3.13/site-packages/typing_inspection/typing_objects.py": 16856718240972376883, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/six.py": 1161259802604920635, + "venv/lib/python3.13/site-packages/pandas/tests/internals/test_internals.py": 2049484047553296860, + "backend/alembic/versions/543d43d752b8_migrate_old_dictionary_entries_and_.py": 4586402369330679789, + "venv/lib/python3.13/site-packages/pygments/lexers/usd.py": 934820244673741695, + "docs/api/html/group__test__chart__creation__failure.html": 15228478008422010264, + "docs/api/html/group____format__permission__key.html": 15264515637376777742, + "venv/lib/python3.13/site-packages/pandas/api/__init__.py": 1145864207387918302, + "venv/lib/python3.13/site-packages/_pytest/_py/path.py": 8464061867075119556, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tanh.csv": 6674536825156196036, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine_first.py": 3100691245280812910, + "docs/api/html/group__ReportsFilterPerformanceTest__Module.html": 6300674926783924392, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_concat.py": 1351478313334379336, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype.pyi": 17662618580381221543, + "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.pyi": 9911899303567570646, + "venv/lib/python3.13/site-packages/PIL/GdImageFile.py": 16875784236394545058, + "docs/api/html/group__test__metrics__empty__job.html": 2409072479669749995, + "venv/lib/python3.13/site-packages/pydantic_settings/utils.py": 14396350455520958703, + "backend/src/models/dataset_review.py": 18282368342351080120, + "docs/api/html/group____enforce__session__version.html": 8264349099371763057, + "venv/lib/python3.13/site-packages/pygments/lexers/boa.py": 14409319610243925864, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_iter.py": 9232021002336641851, + "docs/api/html/group__orchestrator__cancel.html": 7070873577920045293, + "venv/lib/python3.13/site-packages/pillow.libs/libzstd-761a17b6.so.1.5.7": 17875236748140157032, + "venv/lib/python3.13/site-packages/passlib/crypto/__init__.py": 16409196868630049365, + "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.pyi": 8232620851756907417, + "docs/api/html/group__test__create__admin__is__idempotent__for__existing__user.html": 11387718106316703006, + "venv/lib/python3.13/site-packages/uvicorn/loops/auto.py": 13086005977087619545, + "venv/lib/python3.13/site-packages/numpy/_core/_simd.cpython-313-x86_64-linux-gnu.so": 12609666221355091416, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_case_justify.py": 8204537837649550062, + "venv/lib/python3.13/site-packages/pydantic/_internal/_import_utils.py": 4029157986210295967, + "docs/api/html/group__normalizer.html": 9731908195312484209, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_function.py": 13492077564015386528, + "docs/api/html/group____dashboard__git__filter__value.html": 5762878789386422054, + "docs/api/html/group__MapColumnsRequest.html": 17510027445054962, + "venv/lib/python3.13/site-packages/pandas/core/tools/times.py": 6560679305255812809, + "docs/api/html/group__test__json__load__if__needed__empty__string.html": 4809024742473574705, + "venv/lib/python3.13/site-packages/click/termui.py": 7022374827798605437, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/nditer.py": 10038360623032425884, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_pad.pyi": 14755435694146003896, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_indexing.py": 16623126293364224089, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_usecols_basic.py": 16382444980424010514, + "docs/api/html/group__TestReportAuditImmutability.html": 17837908356806354015, + "docs/api/html/group__TranslationJobDictionary.html": 12589120419165244757, + "venv/lib/python3.13/site-packages/pyasn1/type/tag.py": 13912273274509186651, + "venv/lib/python3.13/site-packages/dateutil/utils.py": 7067026915090881998, + "docs/api/html/group__test__list__with__keys.html": 10936269121502713168, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_join.py": 11910166566689889389, + "docs/api/html/search/groups_13.js": 5984573013641066016, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_numba.py": 203614377324678234, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_query_eval.py": 17613800806748649144, + "docs/api/html/group__SessionEvent.html": 1211750384704363849, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dh.pyi": 3254633032236269842, + "venv/lib/python3.13/site-packages/charset_normalizer/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/devicetree.py": 9820277168995231927, + "frontend/build/_app/immutable/chunks/BkZf1CHl.js": 7672076383279571008, + "backend/src/core/superset_client/_dashboards_list.py": 18014687561732553425, + "backend/src/api/routes/__tests__/test_profile_api.py": 10215800611882169470, + "frontend/src/lib/stores/sidebar.js": 4584261073573678043, + "docs/api/html/group__get__my__preference.html": 12554124199014209894, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/METADATA": 317417773265681256, + "venv/lib/python3.13/site-packages/numpy/ma/mrecords.pyi": 8945514863366384484, + "docs/api/html/group__test__migrate__dashboards__env__not__found.html": 17516663970497083336, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/persistence.py": 17908437937265384125, + "venv/lib/python3.13/site-packages/numpy/_core/defchararray.pyi": 2277933368373498515, + "docs/api/html/group__publish__candidate__endpoint.html": 8010913513962824406, + "venv/lib/python3.13/site-packages/numpy/core/_utils.py": 14108063453670701289, + "venv/lib/python3.13/site-packages/starlette/schemas.py": 697416731579555195, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_observance.py": 18178414966262524534, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_utils.py": 11574890848475750509, + "frontend/build/_app/immutable/assets/9.tn0RQdqM.css": 15130871412783076140, + "docs/api/html/group__extract__tables__from__jinja.html": 5053769374599061373, + "docs/api/html/group____FakeSessionDel.html": 7361382465314395458, + "venv/lib/python3.13/site-packages/pandas/_testing/_warnings.py": 5640964566228306285, + "frontend/src/components/tasks/TaskResultPanel.svelte": 2978429516796744791, + "backend/src/api/routes/dashboards/_detail_routes.py": 8372068544868033099, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/random.pyi": 9425735458050148191, + "venv/lib/python3.13/site-packages/attr/validators.pyi": 15579625896285277321, + "docs/api/html/group__GitManagerUnfinishedMergeIntegrationTest__Module.html": 5158591590774869996, + "docs/api/html/group__list__files.html": 13944554477611206786, + "docs/api/html/group__removeToast__Function.html": 7201312866448785982, + "docs/api/html/group__CleanReleaseV2Api.html": 4541756624375891819, + "venv/lib/python3.13/site-packages/cffi/_cffi_errors.h": 1479575326813300292, + "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.pyi": 2667658052734929089, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/exceptions.py": 7518613450390473990, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__datasource.py": 1747755531265478106, + "venv/lib/python3.13/site-packages/numpy/lib/introspect.pyi": 12239924839764265507, + "docs/api/html/group__build__idempotency__key.html": 210393855646189124, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_app.py": 14788764260762890368, + "docs/api/html/group__get__task__log__sources.html": 10195752095365161863, + "docs/api/html/group__fetchRuns__Function.html": 10568246588417794919, + "docs/api/html/group__test__run__without__manifest__rejected.html": 13460920870647693397, + "docs/api/html/group__test__missing__secret__key__crashes.html": 8704844476215085040, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi": 16934250836612138541, + "docs/api/html/group__test__revoke__already__revoked.html": 5813717002399486452, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/common.py": 9354442374897565498, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_object.py": 9215781825358309147, + "frontend/src/lib/api/assistant.js": 12514226537527897950, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/char.pyi": 1639810799219814643, + "backend/src/api/routes/reports.py": 11259252279199691691, + "docs/api/html/group__Navbar.html": 3616287054974977311, + "venv/lib/python3.13/site-packages/_pytest/main.py": 931018496411984909, + "docs/api/html/group__test__parsed__native__filters__model.html": 11879713945172896956, + "docs/api/html/group__accept__preview__session.html": 5962878640752637722, + "build.sh": 18157952417475065314, + "docs/api/html/group____seed__candidate__with__report.html": 5421169190057147596, + "venv/lib/python3.13/site-packages/_pytest/recwarn.py": 15312759837838083596, + "venv/lib/python3.13/site-packages/numpy/dtypes.pyi": 6713822658171717369, + "docs/api/html/group__test__progress__log.html": 16396212906693979688, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/naming.py": 14503932124671438340, + "docs/api/html/search/groups_3.js": 13945080001959834321, + "venv/lib/python3.13/site-packages/urllib3/fields.py": 9503202008440141421, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/json_schema_test_suite.py": 6152158310190822762, + "venv/lib/python3.13/site-packages/numpy/core/multiarray.py": 18368443598659711836, + "docs/api/html/group__MaintenanceBannerPlugin.html": 17523541172049741190, + "venv/lib/python3.13/site-packages/pandas/_testing/_io.py": 14264259716673715185, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_year.py": 1478857009124807836, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/RECORD": 12598309574903741388, + "venv/lib/python3.13/site-packages/charset_normalizer/md__mypyc.cpython-313-x86_64-linux-gnu.so": 12629680116730190225, + "venv/lib/python3.13/site-packages/yaml/scanner.py": 8554164423030397366, + "venv/lib/python3.13/site-packages/pandas/api/executors/__init__.py": 16954693181142010028, + "docs/api/html/group__test__models.html": 5352406217241507006, + "docs/api/html/group__handleSelect__Function.html": 16158838263000329677, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_like.pyi": 1968570015765106620, + "docs/api/html/group__GitGiteaRoutes.html": 17891704073558062610, + "backend/tests/core/test_defensive_guards.py": 5370593078226893371, + "venv/lib/python3.13/site-packages/pygments/lexers/_scilab_builtins.py": 17453812809700086888, + "venv/lib/python3.13/site-packages/numpy/f2py/symbolic.pyi": 14558091964387571257, + "docs/api/html/group__params.html": 10578394165537584451, + "docs/api/html/group__TestAPIKeyAuth.html": 9836588749523231088, + "docs/api/html/group__handleSaveGlobal__Function.html": 5196092168626458120, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/measure.py": 18327922742808984428, + "docs/api/html/group__ClarificationAnswerResultResponse.html": 9302017620017253639, + "venv/lib/python3.13/site-packages/pandas/tests/libs/test_hashtable.py": 7762405492052106717, + "venv/lib/python3.13/site-packages/pygments/lexers/amdgpu.py": 17249439604111838979, + "venv/lib/python3.13/site-packages/tzlocal/py.typed": 15130871412783076140, + "docs/api/html/group__ScreenshotService.html": 3636441712660943738, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/resource_protector.py": 14354891358060085360, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/RECORD": 7844752443022514265, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/METADATA": 4954909612926293404, + "docs/api/html/group__AppConfig.html": 11574004031912899139, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/fallback.py": 4260636031748548345, + "docs/api/html/group__test__generate__docs__empty__ids.html": 1697382171673264282, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_dtype.py": 12210524152630532747, + "venv/lib/python3.13/site-packages/PIL/ImageSequence.py": 16459886231802763032, + "venv/lib/python3.13/site-packages/pyasn1/codec/ber/encoder.py": 8912119133548489544, + "venv/lib/python3.13/site-packages/packaging/licenses/_spdx.py": 16440031845441362300, + "venv/lib/python3.13/site-packages/pygments/lexers/robotframework.py": 11507713622179102496, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/__init__.py": 13749066369790839334, + "docs/api/html/group__test__environment__model.html": 11941389798995546650, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__validate__update__payload.html": 17271010803443567335, + "docs/api/html/group__DistributionManifest.html": 4125661865258730357, + "docs/api/html/group__create__validation__policy.html": 17844050931400995860, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/index_tricks.pyi": 6035507586990711471, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_converters.py": 8869440774594127419, + "docs/api/html/group__openGitManagerForDashboard__Function.html": 13281219317667598991, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_sparse.py": 14343335524508450058, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_repr.py": 5577699046802107144, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py": 9931394693284776391, + "docs/api/html/group__test__verify__password__empty__hash.html": 5792654688365761152, + "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.py": 4037074830103951000, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_level_values.py": 12490886203425393290, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/validator_creation.py": 2745563768861102999, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py": 2609365889369107154, + "backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py": 5075014721013543720, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/WHEEL": 5990830714395966792, + "venv/lib/python3.13/site-packages/pandas/io/formats/info.py": 215303194287834907, + "venv/lib/python3.13/site-packages/git/repo/fun.py": 9019364012746153312, + "backend/src/services/clean_release/repositories/candidate_repository.py": 1901083195512401226, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_textreader.py": 388955620768371652, + "docs/api/html/group__ValidationTaskUpdate.html": 5117191705439943824, + "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/METADATA": 4975169119201255384, + "venv/lib/python3.13/site-packages/ecdsa/der.py": 9374566189581725372, + "docs/api/html/group____serialize__session__summary.html": 12633044514321560047, + "docs/api/html/group__GitServerConfigSchema.html": 14623914455240364180, + "venv/lib/python3.13/site-packages/charset_normalizer/legacy.py": 1706599349563920562, + "backend/alembic/env.py": 17779798763201538947, + "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key__slug.html": 10954317903886069968, + "docs/api/html/search/functions_15.js": 3151355335604935323, + "docs/api/html/group__MaintenanceRoutesModule.html": 17831095570734372209, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/__init__.py": 10456333337205488584, + "venv/lib/python3.13/site-packages/pygments/lexers/yara.py": 17510792615476493957, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_regression.py": 6742646011852052645, + "backend/src/services/clean_release/repositories/compliance_repository.py": 2888085832526178946, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/requirements.py": 11478648568700804705, + "venv/lib/python3.13/site-packages/numpy/strings/__init__.py": 6523674574341354095, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_aix.h": 13316272821279308986, + "frontend/src/lib/ui/index.ts": 15771537668192298194, + "frontend/build/_app/immutable/nodes/24.H1GvTZ14.js": 13161443850709802796, + "docs/api/html/group__test__manifest__service__rejects__missing__candidate.html": 17562104709800078179, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/base.py": 10200916670889920659, + "docs/api/html/group__StoragePluginPackage.html": 6430288179557924153, + "docs/api/html/globals_t.html": 18109595601088428353, + "docs/api/html/group____extract__created__id.html": 16195882368814063060, + "venv/lib/python3.13/site-packages/pandas/_libs/missing.cpython-313-x86_64-linux-gnu.so": 1990755879067132155, + "docs/api/html/group__SessionEventLoggerModule.html": 766072337035773703, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py": 14454690938977069049, + "backend/tests/test_api_key_model.py": 3048384406901125340, + "docs/api/html/group__test__end__all__active__events.html": 15618808237650024096, + "docs/api/html/group__HealthPageIntegrationTest__Module.html": 8263116403958014254, + "venv/lib/python3.13/site-packages/pydantic/dataclasses.py": 12384956801113860889, + "venv/lib/python3.13/site-packages/websockets/imports.py": 1995356703044158461, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numerictypes.py": 5719374287454872555, + "docs/api/html/group__ValidationRunResponse.html": 17135497457065164402, + "venv/lib/python3.13/site-packages/pydantic/v1/main.py": 213087273729911492, + "docs/api/html/group__TranslateCorrectionsApi.html": 9939568944183336332, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/exceptions.py": 14982764273485726199, + "docs/api/html/group__loadSettings__Function.html": 6940780943269890952, + "docs/api/html/group__SupersetClientGetDatasetLinkedDashboardCount.html": 4679132333842497993, + "docs/api/html/group__handleCreate__Function.html": 16671349204777339326, + "venv/lib/python3.13/site-packages/cffi/api.py": 5325821560978502135, + "docs/api/html/group__TestComplianceExecutionService.html": 5266217464530796767, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_astype.py": 11386658862732393123, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/connection.py": 15405047955974108860, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatter.py": 16499277113779414101, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict_of_blocks.py": 11646835537993064324, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/sqltypes.py": 2952996080192295188, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py": 2833016678144975576, + "venv/lib/python3.13/site-packages/pyasn1/compat/__init__.py": 5902203086150636670, + "docs/api/html/group__health__store__Store.html": 8056741375988649819, + "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.py": 503331864022631297, + "venv/lib/python3.13/site-packages/starlette/requests.py": 4582424111350387010, + "docs/api/html/group__SemanticFieldEntryDto.html": 7770402299955438341, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/response.py": 9594165377270752433, + "docs/api/html/group__MockEnvPublic.html": 16155621137935179699, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/state_changes.py": 11425629729736599320, + "venv/lib/python3.13/site-packages/urllib3/contrib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jose/backends/__init__.py": 16581763658239070112, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_extint128.py": 11074657869101074458, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/RECORD": 5612689057373270213, + "backend/tests/services/clean_release/test_demo_mode_isolation.py": 15017454272480710246, + "venv/lib/python3.13/site-packages/_pytest/_code/source.py": 9488112979192923739, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_constructors.py": 5627819885594450614, + "venv/lib/python3.13/site-packages/pandas/core/computation/ops.py": 18250656489649699560, + "venv/lib/python3.13/site-packages/ecdsa/_sha3.py": 10353612776869489378, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/pkg_resources.py": 14271077832999370068, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/fastapi/_compat/may_v1.py": 9880083742927480960, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reduce.py": 14976712507854054776, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_internal/models/format_control.py": 12916176880659590456, + "docs/api/html/group__Imports__Block.html": 3093618662848079957, + "venv/lib/python3.13/site-packages/PIL/_avif.pyi": 18222325750818585549, + "backend/src/plugins/llm_analysis/__tests__/test_screenshot_service.py": 359905908083162759, + "docs/api/html/group__DatasetReviewApiTests.html": 17203814141092898592, + "docs/api/html/group__test__api__get__job__not__found.html": 11032532759238415335, + "venv/lib/python3.13/site-packages/numpy/core/_dtype.pyi": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/linalg/__init__.pyi": 18409133021422984774, + "docs/api/html/globals_a.html": 15978428091419449896, + "venv/lib/python3.13/site-packages/pandas/core/indexes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jaraco/context/__init__.py": 10975097417056278427, + "docs/api/html/group__parse__dataset__selection.html": 6178748760718774225, + "docs/api/html/group____recover__from__markdown.html": 9363288958314889837, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_file.py": 5180401483285462901, + "venv/lib/python3.13/site-packages/pandas/core/groupby/groupby.py": 5536922766239875007, + "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.py": 13446184230476677032, + "venv/lib/python3.13/site-packages/pandas/api/internals.py": 5188134630996087511, + "docs/api/html/group__get__features.html": 12928666550710504587, + "venv/lib/python3.13/site-packages/urllib3/util/retry.py": 17988032431204662756, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/halffloat.h": 16913381775994652343, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpp": 4590574206187223887, + "backend/src/plugins/translate/preview_resolve_provider.py": 7349483761713339246, + "backend/git_repos/remote/test-repo.git/objects/93/9cc583a18267406014c57617ccb557569f6aaf": 5474754694741999746, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__DatasetMapperModule.html": 15394484234769389552, + "venv/lib/python3.13/site-packages/pygments/lexers/compiled.py": 8215290039991205531, + "frontend/build/_app/immutable/nodes/15.Dq8xyDb-.js": 5279337498044861551, + "docs/api/html/group__ReviewWorkspaceHelpers.html": 1176940369643831408, + "frontend/src/lib/stores/__tests__/test_activity.js": 13851679930048900037, + "venv/lib/python3.13/site-packages/apscheduler/util.py": 11440701924954413440, + "backend/src/api/routes/__tests__/test_clean_release_v2_api.py": 17447140422807500877, + "docs/api/html/group__add__dictionary__entry.html": 15530172718575929284, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/markers.py": 5247208668583898949, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_validate.py": 2133732492454460539, + "docs/api/html/group__run__cleanup.html": 10946982331286352210, + "docs/api/html/group__TaskList.html": 17909740066705680833, + "backend/git_repos/remote/test-repo.git/objects/c5/b992984fe856bd81206304a881faa3087937cf": 2441623644664166468, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/models.py": 9513172366009392193, + "docs/api/html/group__TargetSchemaHintTest.html": 14589542991368144718, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_integer.py": 16102640589300356639, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/_gen_files.py": 6273314671315377131, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.cpython-313-x86_64-linux-gnu.so": 17089839649971421164, + "venv/lib/python3.13/site-packages/pandas/testing.py": 18352975429944961593, + "docs/api/html/group__delete__health__report.html": 7248475607320545469, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/filesize.py": 13234856188049714485, + "venv/lib/python3.13/site-packages/pandas/core/ops/mask_ops.py": 16152832446926420035, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_astype.py": 6578539240745332772, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_update.py": 9098258267671647783, + "backend/src/plugins/translate/prompt_builder.py": 11939931262286362350, + "docs/api/html/group__test__mobile__functions__Function.html": 10962243893129004667, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_numpy.py": 222383250675686477, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/base_server.py": 9643197259080504542, + "venv/lib/python3.13/site-packages/websockets/sync/connection.py": 2791040669094103200, + "docs/api/html/plus.svg": 9293766439275217251, + "venv/lib/python3.13/site-packages/pandas/core/groupby/base.py": 16888506204910366302, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dropna.py": 17272771246760486947, + "backend/src/api/routes/assistant/_tool_llm.py": 5944701624485258223, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_django.py": 8455054295577203929, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_frame.py": 2169381747938271444, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_compression.py": 3167245427092658522, + "docs/api/html/group__OrchestratorHelpers.html": 15580278811762324793, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/METADATA": 6596896092754256988, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_methods.py": 12694335778731418517, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_constructors.py": 11065058462752831997, + "docs/api/html/group__SidebarNavigationTest__Module.html": 17269318974786788900, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/base.py": 13444129650880426353, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate.py": 11205714006000849926, + "venv/lib/python3.13/site-packages/pycparser/ply/ygen.py": 5520340773074632724, + "frontend/src/routes/profile/__tests__/fixtures/profile.fixtures.js": 11794595959206318249, + "docs/api/html/group__LLMTranslationService.html": 12719944626760388569, + "venv/lib/python3.13/site-packages/numpy/core/umath.py": 4476812261188108155, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/__init__.py": 15130871412783076140, + "frontend/playwright-report/data/5fe3cb66319839f5cb12b729db0063421ca049fd.zip": 2843141943658610655, + "venv/lib/python3.13/site-packages/pip/_internal/operations/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/__init__.py": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/objects/99/38d17c3f4199e53e4d4ebc253fe9c840caa505": 8414792171590415839, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/npyio.pyi": 15811156298637131744, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/RECORD": 9461427699726982537, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_select_dtypes.py": 3530442956913622572, + "venv/lib/python3.13/site-packages/pandas/tests/extension/json/array.py": 7437107031079833232, + "backend/src/plugins/translate/superset_executor.py": 6596865985477333081, + "docs/api/html/group__SupersetClientExtractNativeFiltersFromKey.html": 15741298745054631643, + "docs/api/html/group__DictionaryEntryCRUD.html": 6734073441533159576, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/licenses/LICENSE": 8438117004702803716, + "docs/api/html/group____FakeDb.html": 6191058217277272469, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/linalg.pyi": 7217817172953767893, + "venv/lib/python3.13/site-packages/numpy/random/_pickle.py": 7774620933480975313, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_freq_attr.py": 17290447066681837965, + "docs/api/html/group__handleSelectAll__Function.html": 14242974889351038882, + "docs/api/html/group__import__candidate__artifacts__v2__endpoint.html": 11794097000012858321, + "venv/lib/python3.13/site-packages/numpy/_core/multiarray.pyi": 3494871767602648807, + "venv/lib/python3.13/site-packages/_yaml/__init__.py": 3395829783751571350, + "docs/api/html/group__test__release__approve__and__publish__revoke__contract.html": 18374074269031186609, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_add_prefix_suffix.py": 8367793476246001244, + "frontend/src/lib/components/translate/__tests__/test_correction_cell.svelte.js": 7400069550465334990, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_getitem.py": 11882213323878588707, + "venv/lib/python3.13/site-packages/numpy/_typing/_char_codes.py": 14162745886150625150, + "venv/lib/python3.13/site-packages/pydantic/_migration.py": 17982816821930887921, + "docs/api/html/group__checkout__branch.html": 2135218692165280454, + "docs/api/html/group__get__roles__by__ad__groups.html": 5091343705498361491, + "frontend/src/lib/components/dataset-review/SemanticLayerReview.svelte": 2835359394064716026, + "docs/api/html/group__ErrorPage.html": 8821459762904826623, + "docs/api/html/group__handleDelete__Function.html": 16605529400179631647, + "docs/api/html/group__NotificationConfig.html": 15289328478261723384, + "docs/api/html/group__ThrottledSchedulerConfigurator.html": 4428654146760584758, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape_base.pyi": 5046405609030418900, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp.csv": 601782991752922510, + "docs/api/html/group__SQLInsertService.html": 818564019015064565, + "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/_cryptography_key.py": 388489172928969873, + "venv/lib/python3.13/site-packages/passlib/tests/test_win32.py": 13932956220195912963, + "venv/lib/python3.13/site-packages/pandas/_libs/sas.pyi": 873957906443703977, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/memory.py": 9947113720821563254, + "venv/lib/python3.13/site-packages/urllib3/_version.py": 839747992523118887, + "venv/lib/python3.13/site-packages/rapidfuzz/__init__.pyi": 13131938031654453415, + "venv/lib/python3.13/site-packages/pydantic/v1/json.py": 16596969681914729251, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_character.py": 850554362222586199, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_raises.py": 9678418010293693275, + "venv/lib/python3.13/site-packages/pandas/core/internals/ops.py": 4740509262379451243, + "docs/api/html/group__MetricSnapshot.html": 18425939041842608851, + "docs/api/html/group__TranslateModels.html": 12305275874224350979, + "venv/lib/python3.13/site-packages/pandas/core/indexers/objects.py": 409599993084996593, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_constructors.py": 13890224577680389718, + "docs/api/html/group__headers.html": 18390732070560226439, + "venv/lib/python3.13/site-packages/numpy/lib/mixins.py": 7404729635424818497, + "venv/lib/python3.13/site-packages/pandas/core/reshape/concat.py": 2218848863149192467, + "docs/api/html/group__BatchApproveMappingRequest.html": 2370830240828093817, + "venv/lib/python3.13/site-packages/passlib/apache.py": 2357244937629445006, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get_value.py": 389530025760539462, + "venv/lib/python3.13/site-packages/pygments/lexers/c_like.py": 13004061780700827047, + "venv/lib/python3.13/site-packages/pip/_internal/cli/progress_bars.py": 7584699216537478969, + "venv/lib/python3.13/site-packages/pandas/core/computation/check.py": 16048939716341154196, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_unix.h": 13709837360414450184, + "backend/src/plugins/translate/preview_constants.py": 12447768695944862994, + "docs/api/html/group__AuthPackage.html": 9983939503119143826, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/f2cmap/isoFortranEnvMap.f90": 15768643271492041853, + "venv/lib/python3.13/site-packages/pygments/styles/friendly.py": 13799024964803741891, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/METADATA": 4868544280955034757, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py": 7019253290755243568, + "docs/api/html/group__test__valid__key__returns__principal.html": 14343096821860232032, + "venv/lib/python3.13/site-packages/git/config.py": 1724894144987553201, + "docs/api/html/search/groups_2.js": 4162831969457518591, + "backend/src/core/utils/async_network.py": 7880952036357722891, + "venv/lib/python3.13/site-packages/pandas/_libs/indexing.cpython-313-x86_64-linux-gnu.so": 13188040757764159731, + "venv/lib/python3.13/site-packages/pydantic_core/__init__.py": 10563134104783445328, + "venv/lib/python3.13/site-packages/idna-3.11.dist-info/RECORD": 11541505360973709787, + "docs/api/html/group__get__plugin.html": 13473880094770668887, + "docs/api/html/group__get__task__log__level.html": 8101832559719739013, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/array.py": 13840510238256626632, + "venv/lib/python3.13/site-packages/pydantic/_internal/_known_annotated_metadata.py": 9308620143218174987, + "venv/lib/python3.13/site-packages/pygments/styles/gh_dark.py": 7717523746712391809, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/clsregistry.py": 1589692649837935355, + "docs/api/html/group__test__expired__confirmation__cannot__be__confirmed.html": 1867999819644325388, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs7.pyi": 2777456956260371820, + "docs/api/html/group____is__conversation__archived.html": 601991753846203755, + "frontend/build/_app/immutable/chunks/2dOUpm6k.js": 11811637653151731489, + "venv/lib/python3.13/site-packages/sqlalchemy/connectors/pyodbc.py": 4146960201984489604, + "docs/api/html/group__goToPage__Function.html": 17867667754784109519, + "docs/api/html/group__UnifiedReportsPage.html": 4964051768663825925, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/RECORD": 15194367076254806710, + "frontend/src/lib/stores/__tests__/taskDrawer.test.js": 9115135514077743547, + "docs/api/html/group__GitServiceModule.html": 11876529200168327141, + "frontend/src/components/git/DeploymentModal.svelte": 3761818357348583741, + "frontend/build/_app/immutable/chunks/jF8fpqVn.js": 11630601570352884280, + "venv/lib/python3.13/site-packages/websockets/version.py": 5853118844880112885, + "docs/api/html/topics.js": 6653823426024009041, + "docs/api/html/group__ClarificationStatus.html": 10157970874585041006, + "venv/lib/python3.13/site-packages/numpy/f2py/__init__.pyi": 3566887042414273892, + "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/trace.zip": 2843141943658610655, + "venv/lib/python3.13/site-packages/pip/_internal/commands/list.py": 13035925292530461722, + "docs/api/html/group__test__get__next__executions__invalid.html": 11892479945639081834, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_rewrite_warning.py": 4895919872954538758, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reshaping.py": 18085648077811502689, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dtypes.py": 7550022036887594386, + "docs/api/html/group__test__api__bulk__corrections.html": 13663959216271829060, + "venv/lib/python3.13/site-packages/certifi/__init__.py": 9045580998764948967, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/errors.py": 4817746830102488448, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_character.py": 17168707712220699738, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ma.pyi": 6467636381637286867, + "docs/api/html/group__GitServiceSyncMixin.html": 5716850320253869040, + "frontend/build/_app/env.js": 8815854342083926790, + "docs/api/html/group__generate__uuid.html": 5567603167679991463, + "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.py": 1644743742545329282, + "docs/api/html/group__get__remote__ids__batch.html": 13066542161010026756, + "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.py": 12251264586530536622, + "docs/api/html/group__test__list__runs__with__data.html": 12537031394595357439, + "venv/lib/python3.13/site-packages/gitdb/db/mem.py": 15823812455996199993, + "docs/api/html/group__TranslateJobService.html": 14826837477872478202, + "docs/api/html/group__ImportedFilterDto.html": 5501399725886626676, + "venv/lib/python3.13/site-packages/jsonschema/protocols.py": 847332300543820834, + "dist/docker/ss-tools-enterprise-0.1.6.bundle": 17219824645252541519, + "venv/lib/python3.13/site-packages/requests/status_codes.py": 17005213506461710202, + "docs/api/html/group__get__dashboard__detail.html": 10495893496001819056, + "docs/api/html/group__handleUpdateSchedule__Function.html": 3932290098337453365, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_freq_attr.py": 1720704043778272895, + "venv/lib/python3.13/site-packages/fastapi/security/oauth2.py": 1925394177931547776, + "docs/api/html/group__has__environments.html": 1097953362161131768, + "docs/api/html/group__test__authenticate__user.html": 9321693170809204808, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/__init__.py": 11709551233929312136, + "docs/api/html/group__DictionaryCreate.html": 5296179021197586468, + "venv/lib/python3.13/site-packages/pygments/lexers/wren.py": 6259586056260713984, + "venv/lib/python3.13/site-packages/jose/constants.py": 10880650415673457194, + "venv/lib/python3.13/site-packages/urllib3/poolmanager.py": 15431313180245757657, + "docs/api/html/group__test__add__logs__empty.html": 5564646933686414085, + "docs/api/html/group__delete__tasks.html": 2265288733173423107, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_equals.py": 6429101196875333574, + "venv/lib/python3.13/site-packages/_pytest/pytester.py": 5423287282155948827, + "docs/api/html/group__SupersetClientGetAllResources.html": 16201935844007355283, + "docs/api/html/group__TestSecurityAuditFixes.html": 9553894818504109298, + "docs/api/html/folderclosedd.svg": 17609606181638232885, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_real.py": 5283415082530228716, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel.pyi": 16372699564794732463, + "backend/src/core/migration/archive_parser.py": 16510017196974398795, + "venv/lib/python3.13/site-packages/jeepney/tests/test_bus_messages.py": 4323093138307306385, + "docs/api/html/group__test__dataset__dashboard__relations__script.html": 682612274542588295, + "docs/api/html/group__InlineCorrectionService.html": 11403442380597184495, + "venv/lib/python3.13/site-packages/rsa/pkcs1.py": 2576732594642491910, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_laguerre.py": 7475592201750602042, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/revocation.py": 5718403706971163960, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.pyi": 15789956857032062129, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_get_dummies.py": 14871264482622327003, + "docs/api/html/group______Function.html": 11167138341912770463, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nditer.pyi": 2351219156711800159, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pyodbc.py": 11271266527234292020, + "docs/api/html/group__test__belief__scope__success__reflect.html": 8691621763793235095, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_tooltip.py": 853808724844451770, + "docs/api/html/group__auth__callback__adfs.html": 14958207000323215557, + "venv/lib/python3.13/site-packages/greenlet/greenlet.h": 16216120538647882082, + "docs/api/html/group__test__list__jobs.html": 3648457057716850116, + "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.pyi": 9854069702366900663, + "docs/api/html/group__resolve__task__type.html": 14911663331335466278, + "venv/lib/python3.13/site-packages/pydantic/typing.py": 6661252174518316760, + "backend/src/api/routes/assistant/_tool_backup.py": 1220805467504610548, + "docs/api/html/group____extract__result__payload.html": 866721391193515598, + "docs/api/html/group__state__mock__Module.html": 17946734577134518363, + "venv/lib/python3.13/site-packages/PIL/PixarImagePlugin.py": 12061717157676819554, + "docs/api/html/group__ProfileSettingsStateIntegrationTest.html": 16949360457251592220, + "docs/api/html/group__loadStatus__Function.html": 424560253324877602, + "docs/api/html/search/mag_seld.svg": 5172700322779250184, + "docs/api/html/group__detect__virtual__columns.html": 7311963466156624138, + "docs/api/html/group__normalize__density.html": 4520562612418312413, + "venv/lib/python3.13/site-packages/click/_winconsole.py": 8133229457213704188, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/__init__.py": 3577295959173028042, + "venv/lib/python3.13/site-packages/dotenv/parser.py": 17651523026132242005, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_ufunclike.py": 13752381070039804384, + "frontend/playwright-report/trace/codicon.DCmgc-ay.ttf": 15579197054804464248, + "backend/src/api/routes/llm.py": 14720807640771498486, + "docs/api/html/group__run__compliance__run.html": 16684598694182162144, + "docs/api/html/group__test__get__next__executions.html": 17453367342374515739, + "docs/api/html/group__test__get__dashboards__superset__failure.html": 4480500139441012004, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_retain_attributes.py": 16210262356844956764, + "backend/_convert_defs.py": 13451549787311898273, + "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe.py": 16525846817331842080, + "docs/api/html/group__SettingsPage.html": 11139530217038362934, + "backend/src/plugins/translate/orchestrator_lang_stats.py": 15316337409127730060, + "frontend/src/routes/datasets/review/review-workspace-helpers.js": 10463352127599084718, + "docs/api/html/group__approval__repository.html": 7564608071995148831, + "docs/api/html/group__HealthStore.html": 7646703134391863590, + "docs/api/html/group__AuthModels.html": 12156417709466649370, + "docs/api/html/group__TranslationPreviewResponse.html": 10623704899221380546, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asfreq.py": 2701081989348180239, + "docs/api/html/group__candidate__repository.html": 7211404956141328496, + "docs/api/html/group__delete__logs__for__task.html": 14542367631920374192, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__downloadFile__Function.html": 6136195968307907091, + "docs/api/html/group__resumeTask__Function.html": 15320819225352081425, + "docs/api/html/group__test__release__candidate__empty__id.html": 12372974039816984488, + "docs/api/html/group__test__legacy__prepare__endpoint__still__available.html": 4064504421429684226, + "docs/api/html/search/all_12.js": 9323517322081325944, + "docs/api/html/group__selectedTask__Data.html": 6917775707953198812, + "venv/lib/python3.13/site-packages/numpy/lib/_shape_base_impl.py": 16539612003028676816, + "venv/lib/python3.13/site-packages/numpy/lib/_iotools.py": 10522826373041592861, + "venv/lib/python3.13/site-packages/pip/_internal/commands/freeze.py": 8644678595442275485, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/histograms.pyi": 2281259341486424755, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/METADATA": 3188324716322823755, + "docs/api/html/group__update__banner__markdown__content.html": 5764762595078036751, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_update.py": 12293424926920594944, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arrow_compat.py": 12986187285988432338, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/plugin_base.py": 2880434680251108786, + "venv/lib/python3.13/site-packages/pandas/io/formats/console.py": 10370334487585281127, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/provision.py": 17812733620485633975, + "frontend/e2e/tests/migration.e2e.js": 7453107045608888506, + "frontend/src/components/git/BranchSelector.svelte": 4848847079896357068, + "backend/src/services/clean_release/publication_service.py": 7731809712174358922, + "backend/src/plugins/translate/service.py": 14983236479693051345, + "venv/lib/python3.13/site-packages/pygments/lexers/roboconf.py": 1335271183806115470, + "docs/api/html/group__TargetSchemaValidationRequest.html": 12191974137983183185, + "docs/api/html/group__persist__tasks.html": 11196063061437802602, + "docs/api/html/nav_h.png": 18087931637685379732, + "venv/lib/python3.13/site-packages/anyio/abc/__init__.py": 11428476337116642278, + "venv/lib/python3.13/site-packages/passlib/tests/test_registry.py": 15377817946543064293, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_fillna.py": 9297637486401098611, + "docs/api/html/group__parse__llm__response.html": 3772780281405997991, + "docs/api/html/group__DashboardTypes.html": 9536763043691441999, + "docs/api/html/group__DICT__TOKENS__PER__ENTRY.html": 6326579402835354804, + "venv/lib/python3.13/site-packages/numpy/random/_mt19937.cpython-313-x86_64-linux-gnu.so": 85866616759631810, + "venv/lib/python3.13/site-packages/pandas/core/util/numba_.py": 3535857372998653863, + "venv/lib/python3.13/site-packages/pandas/core/arraylike.py": 11439602957100017713, + "backend/src/plugins/translate/scheduler.py": 13650497976959209825, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/mod.pyi": 10591120373577657317, + "backend/src/services/clean_release/approval_service.py": 14346840951852454312, + "docs/api/html/search/all_16.js": 6930966498952410677, + "docs/api/html/group____require__execution__flag.html": 5795997050372262927, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77comments.f": 4801926170672147659, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_strings.py": 7168544888340296373, + "docs/api/html/group__SupersetAPIError.html": 5887592897373484111, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling.py": 18221459425751198218, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/RECORD": 10036839994791510159, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resampler_grouper.py": 8388490036390951341, + "docs/api/html/group__MappingResponse.html": 14098548275925135132, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_fillna.py": 4610666845291315333, + "docs/api/html/navtreeindex20.js": 6185764429877814972, + "docs/api/html/group__rbac__permission__catalog.html": 4836642153723426530, + "docs/api/html/group__get__settings.html": 1490728116106832265, + "frontend/src/assets/svelte.svg": 13872699371039797074, + "frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte": 7963394462495599501, + "docs/api/html/group__t__Store.html": 5003719811547922783, + "venv/lib/python3.13/site-packages/authlib/oidc/core/errors.py": 18154419146257747681, + "docs/api/html/group__DashboardHub__toggleFilterDropdown__Function.html": 2486046615713604253, + "docs/api/html/globals_func_m.html": 5387554071243242205, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/__init__.py": 2246704695838251846, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test__iotools.py": 6260912730310453037, + "backend/src/services/notifications/__tests__/test_notification_service.py": 13027609991239837872, + "docs/api/html/group__run__enterprise__clean__e2e.html": 6563961649768557058, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_common.py": 2223069451949206319, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/live.py": 8586058300967410643, + "venv/bin/jsonschema": 16516374846693491442, + "venv/lib/python3.13/site-packages/rpds/__init__.pyi": 12037474523494903587, + "docs/api/html/group__test__extract__chart__uuids__from__archive.html": 3048505118835977338, + "backend/tests/test_maintenance_service.py": 11188820692455665244, + "backend/src/models/dataset_review_pkg/_clarification_models.py": 5123904878653517000, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_unique.py": 16285418015531891530, + "venv/lib/python3.13/site-packages/jaraco/functools/__init__.py": 15334412919279565528, + "venv/lib/python3.13/site-packages/greenlet/tests/__init__.py": 1181423074160870921, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/types.py": 356973910046950951, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_util.py": 6573034224727513309, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sinh.csv": 12358602501260667480, + "docs/api/html/group____coerce__query__bool.html": 12206666042007651766, + "docs/api/html/group__SupersetContextExtractorPackage.html": 16163237109197150819, + "venv/lib/python3.13/site-packages/packaging/markers.py": 11602308203217831543, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml_dtypes.py": 18293138335799308439, + "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.pyi": 18253950285509265097, + "docs/api/html/group__test__update__schedule.html": 10218439875766465243, + "docs/api/html/group__get__type__profile.html": 8801753117531383071, + "docs/api/html/group__selectTask__Function.html": 13172211009893761565, + "docs/api/html/group____build__target__columns.html": 5216268696007966716, + "venv/lib/python3.13/site-packages/numpy/core/records.py": 12604652690721760437, + "venv/lib/python3.13/site-packages/numpy/strings/__init__.pyi": 6213864891045042045, + "venv/lib/python3.13/site-packages/anyio/abc/_tasks.py": 16839170934610571514, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_backend.py": 17793139799410518847, + "venv/lib/python3.13/site-packages/urllib3/connectionpool.py": 7493427857878469165, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/watchfilesreload.py": 13062440477475586751, + "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/ext.py": 15699809032725838985, + "docs/api/html/group__test__enterprise__policy__external__allowed.html": 13390827241810832379, + "venv/lib/python3.13/site-packages/pandas/core/reshape/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayprint.pyi": 2053388407376896377, + "venv/lib/python3.13/site-packages/jeepney/io/tests/utils.py": 10346859893683522113, + "venv/bin/pip": 13861749540792881808, + "docs/api/html/group__test__update__task__success.html": 12948386744398037759, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_methods.py": 14881910898871057761, + "venv/lib/python3.13/site-packages/pandas/core/computation/align.py": 13027916530558901817, + "venv/lib/python3.13/site-packages/tzlocal/unix.py": 995019116408448073, + "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.py": 14034239842770171660, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/help.py": 3124654001287175629, + "docs/api/html/group__test__trigger__validation__skips__no__dashboards.html": 14447781900606710025, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/WHEEL": 7454950858448014158, + "venv/lib/python3.13/site-packages/websockets/asyncio/router.py": 15039562376314233669, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/__init__.py": 7062045842767697645, + "docs/api/html/group__TaskCleanupModule.html": 3445725949613041336, + "venv/lib/python3.13/site-packages/passlib/handlers/sha2_crypt.py": 8171587560325812664, + "venv/lib/python3.13/site-packages/PIL/py.typed": 15130871412783076140, + "docs/api/html/group__assistantChat__Store.html": 18317790308537053696, + "venv/lib/python3.13/site-packages/pygments/lexers/_scheme_builtins.py": 5722444920479775453, + "venv/lib/python3.13/site-packages/fastapi/_compat/model_field.py": 392699021682253569, + "docs/api/html/group__ViolationSeverity.html": 6547432338145120005, + "venv/lib/python3.13/site-packages/pandas/core/reshape/reshape.py": 5387892304726762127, + "docs/api/html/group__frontend__src__lib__utils__debounce__js____debounce.html": 6554406133377600091, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ranges.py": 14549386056660231279, + "venv/lib/python3.13/site-packages/dateutil/tz/_factories.py": 7538654189286887518, + "docs/api/html/group____handle__http__error.html": 9220062881002809882, + "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/conftest.py": 3691712173202219606, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/visitors.py": 5190923415390076080, + "docs/api/html/navtreedata.js": 10541563969521895995, + "venv/lib/python3.13/site-packages/numpy/_core/_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so": 13269324887178340397, + "docs/api/html/group__CommitSchema.html": 8902723025854017338, + "venv/lib/python3.13/site-packages/git/index/typ.py": 11661540784850363024, + "venv/lib/python3.13/site-packages/pygments/lexers/carbon.py": 6753624244809529841, + "docs/api/html/group__TaskLogViewerTest__Module.html": 3664733466330383169, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/parameter.py": 2793853093222900337, + "venv/lib/python3.13/site-packages/pygments/lexers/monte.py": 15158829991357994573, + "docs/api/html/group__ConfirmationRecord.html": 3457268499822373186, + "docs/api/html/group__DatasetReviewUs3UxTests__Module.html": 4798151615875850408, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_network.py": 13159427333040109523, + "frontend/src/lib/api/translate/__tests__/test-target-schema.test.js": 16872327388398551554, + "docs/api/html/group__websocket__endpoint.html": 5701303797721389053, + "docs/api/html/group__mock__profile__route__dependencies.html": 17310608398693875616, + "docs/api/html/group____cleanup__manager.html": 13234621596823816761, + "docs/api/html/group__AUTH__DATABASE__URL.html": 7086189906630592132, + "RUSAL_ROOT.cer": 8918548221484048276, + "backend/git_repos/remote/test-repo.git/objects/5a/82ef700fef88b69cd480e4025283809496ee20": 7003383965314354150, + "venv/lib/python3.13/site-packages/pandas/_libs/ops.pyi": 1192312467209548763, + "venv/lib/python3.13/site-packages/authlib/oidc/registration/claims.py": 4161520263207458028, + "venv/lib/python3.13/site-packages/pandas/core/computation/common.py": 5369352684655954608, + "venv/lib/python3.13/site-packages/_pytest/_io/pprint.py": 11202117031437223527, + "docs/api/html/group__ComplianceStageRun.html": 4711615660021558641, + "venv/lib/python3.13/site-packages/anyio/to_interpreter.py": 8369454842687342401, + "docker/frontend.Dockerfile": 11291701046055590331, + "venv/lib/python3.13/site-packages/numpy/_core/lib/pkgconfig/numpy.pc": 14713682399742251335, + "docs/api/html/group__test__create__schedule.html": 6101710034110852291, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/array.py": 3517673819082574078, + "venv/lib/python3.13/site-packages/pandas/io/sql.py": 12592024569011796790, + "frontend/src/routes/dashboards/health/__tests__/health_page.integration.test.js": 7606382627471479368, + "venv/lib/python3.13/site-packages/urllib3/filepost.py": 3814725067598042920, + "venv/lib/python3.13/site-packages/click/testing.py": 15973659312284678327, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sample.py": 16877779557071324115, + "docs/api/html/group__MappingCreate.html": 12518001484805787826, + "docs/api/html/group__apply__language__action.html": 6258130513680615487, + "venv/lib/python3.13/site-packages/PIL/BlpImagePlugin.py": 7209218638257731375, + "docs/api/html/group__APIKeyPrincipal.html": 17508654367012660787, + "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.cpython-313-x86_64-linux-gnu.so": 5284769758475394095, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_count.py": 16947194157984708529, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_join.py": 2585969863606902743, + "venv/lib/python3.13/site-packages/PIL/ImageTk.py": 17063969714816564357, + "docs/api/html/search/groups_8.js": 9394148223818886948, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/validator.py": 11486125578281617670, + "venv/lib/python3.13/site-packages/websockets/utils.py": 17624459810483092475, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/validator.py": 14200019408839159476, + "venv/lib/python3.13/site-packages/fastapi/openapi/docs.py": 16735218221639179927, + "venv/lib/python3.13/site-packages/numpy/_core/__init__.py": 14951958843029076911, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD": 12733654377842895588, + "venv/lib/python3.13/site-packages/numpy/__config__.pyi": 1422114139050700402, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/METADATA": 15970460108484113417, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_shift.py": 14026124784027171225, + "venv/lib/python3.13/site-packages/pygments/lexers/modula2.py": 6575895980250662093, + "docs/api/html/group__ComplianceStages.html": 10094047934285993702, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/traceback.py": 2535227583726271746, + "docs/api/html/group__start__scheduler.html": 5136832911990580041, + "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__permalink__key.html": 12241603215085833674, + "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.pyi": 2628035888703031029, + "docs/api/html/group__SemanticSourceType.html": 8482820622466037543, + "frontend/src/routes/reports/llm/[taskId]/+page.svelte": 6536814190745452815, + "docs/api/html/group____seed__candidate__and__passed__report.html": 14818107129922209880, + "docs/api/html/group____detect__delimiter.html": 10132233034331217270, + "docs/api/html/group__SessionEventLoggerImports.html": 4770216863181248837, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_stack.py": 5316826578695144945, + "docs/api/html/group__EnvironmentContextStore.html": 3222220561881374991, + "docs/api/html/group__RecommendedAction.html": 7305257791588353178, + "backend/src/services/clean_release/repositories/approval_repository.py": 11060626515832882475, + "docs/api/html/group__RoleCreate.html": 982606771719996224, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.py": 3762934316884129514, + "venv/lib/python3.13/site-packages/keyring/backends/macOS/__init__.py": 1010937986753403630, + "venv/lib/python3.13/site-packages/pillow.libs/libopenjp2-94e588ba.so.2.5.4": 2483101806045986252, + "backend/git_repos/remote/test-repo.git/refs/heads/main": 7667767390913789840, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/impl.py": 4687705587963980337, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_envs.py": 5657151394155576509, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets.py": 4499538899492148499, + "docs/api/html/group__update__banner__on__dashboard.html": 1731887400533112584, + "docs/api/html/group__CleanPolicySnapshot.html": 4218818132714115677, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_cython_aggregations.py": 10818895573637904764, + "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.cpython-313-x86_64-linux-gnu.so": 17856848989827220388, + "docs/api/html/group__ProfileValidationError.html": 8590349442789923081, + "venv/lib/python3.13/site-packages/git/objects/commit.py": 12317865358846331802, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/status.py": 2214590120238144582, + "docs/api/html/group__test__list__tasks__pagination.html": 5445789220112902473, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/WHEEL": 2357997949040430835, + "docs/api/html/group____build__expected__columns.html": 382226139122586930, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/errors.py": 2147619440137604907, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/emoji.py": 9166774203918731896, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/regexopt.py": 16073437303824961930, + "venv/lib/python3.13/site-packages/passlib/handlers/des_crypt.py": 15304795681178011265, + "docs/api/html/group__frontend__src__services__adminService__js____getPermissions.html": 986367622820934646, + "docs/api/html/group__persist__task.html": 17252468204924884629, + "docs/api/html/tab_hd.png": 15081546309524496729, + "venv/lib/python3.13/site-packages/jeepney/low_level.py": 6546206679858571608, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/METADATA": 9041650844081036822, + "venv/lib/python3.13/site-packages/jose/backends/cryptography_backend.py": 2875270748627611635, + "venv/lib/python3.13/site-packages/_pytest/logging.py": 10972553724755511468, + "venv/lib/python3.13/site-packages/sqlalchemy/util/langhelpers.py": 1246582552828542841, + "docs/api/html/group__MigrationArchiveParserModule.html": 245299514416957043, + "venv/lib/python3.13/site-packages/httpcore/__init__.py": 5897259569277532401, + "venv/lib/python3.13/site-packages/pygments/lexers/mips.py": 2078929831513352031, + "venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py": 17461379236189029253, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_npfuncs.py": 6132674295193898715, + "docs/api/html/group__create__isolated__repository.html": 15487512963939648991, + "venv/lib/python3.13/site-packages/PIL/PSDraw.py": 1927169421822446045, + "venv/lib/python3.13/site-packages/pygments/lexers/rust.py": 8605690716125176329, + "backend/src/core/utils/__init__.py": 12454773556575864508, + "docs/api/html/group__TelegramProvider.html": 2082762596768250446, + "frontend/build/_app/immutable/assets/0.CEUU1nVc.css": 222740866690066235, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_gcc.h": 3561524604274567339, + "venv/lib/python3.13/site-packages/httpx/__version__.py": 9473642134297828811, + "docs/api/html/group__shutdown__event.html": 11840402120515204160, + "docs/api/html/group__resolveRepositoryStatusToken__Function.html": 17579339820007621981, + "venv/lib/python3.13/site-packages/tzlocal/win32.py": 1949985224569608978, + "venv/lib/python3.13/site-packages/keyring/backends/fail.py": 5792496235026678021, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_size.py": 8527839563560550806, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_old_base.py": 191359377446257900, + "docs/api/html/group__compliance__repository.html": 5329437978880410491, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/METADATA": 6960169108624303030, + "venv/lib/python3.13/site-packages/pygments/lexers/vyper.py": 6477559246065994199, + "frontend/playwright-report/trace/assets/codeMirrorModule-Ds_H_9Yq.js": 1369559823166915102, + "backend/git_repos/remote/test-repo.git/hooks/sendemail-validate.sample": 8058833260784074657, + "docs/api/html/group__fetchEnvironments__Function.html": 11012013762197915559, + "docs/api/html/group__TaskDrawerTest__Module.html": 14203440361140799361, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.pyi": 7410507092040239447, + "backend/src/api/routes/assistant/_schemas.py": 17540735345133623114, + "venv/lib/python3.13/site-packages/PIL/ImageShow.py": 11437096951764124817, + "docs/api/html/group__TestComplianceTaskIntegration.html": 13584167952680330678, + "venv/lib/python3.13/site-packages/jsonschema/cli.py": 9813914882090789748, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/models.py": 15928924478695638423, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/char.pyi": 13143560686049627445, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufunc_config.py": 14728417910466146460, + "venv/lib/python3.13/site-packages/pygments/lexers/algebra.py": 9651893228421050246, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multiarray.py": 5928288171180750841, + "venv/lib/python3.13/site-packages/_pytest/config/compat.py": 13175516717549204627, + "venv/lib/python3.13/site-packages/passlib/handlers/digests.py": 18158237109145814857, + "venv/lib/python3.13/site-packages/numpy/core/shape_base.py": 17925759824894973734, + "docs/api/html/group__update__language__stats.html": 15508043268515320115, + "venv/lib/python3.13/site-packages/rsa/__init__.py": 14623742580410014505, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/scipy_sparse.py": 2091608752719976909, + "docs/api/html/group__StorageConfig.html": 12738778311037071479, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.BSD": 3858254215169937333, + "backend/git_repos/remote/test-repo.git/objects/6f/85e20ce3ef98cf07a2f102498b71b9462552c5": 3012017807908986452, + "docs/api/html/group__policy__repository.html": 8836991613608383932, + "docs/api/html/group__get__merge__status.html": 5585461605770877372, + "venv/lib/python3.13/site-packages/anyio/to_process.py": 16627578499791368365, + "venv/lib/python3.13/site-packages/fastapi/staticfiles.py": 4094330072433302614, + "venv/lib/python3.13/site-packages/PIL/TarIO.py": 13651401276968618561, + "venv/lib/python3.13/site-packages/urllib3/http2/__init__.py": 62108352209996836, + "backend/src/api/routes/translate/_metrics_routes.py": 14203374652620439805, + "docs/api/html/group__unsubscribe__dataset__events.html": 1558763019678339438, + "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_period.py": 10309599277099605395, + "docs/api/html/group__get__datasource__columns.html": 14552107166235009597, + "docs/api/html/group____parse__sqllab__result.html": 10462280637644525478, + "docs/api/html/group__test__reload__validation__policy__adds__job.html": 1850773905928943851, + "venv/lib/python3.13/site-packages/numpy/lib/scimath.pyi": 18165025261847276375, + "docs/api/html/group__SettingsSchemas.html": 7781964904235893244, + "venv/lib/python3.13/site-packages/pandas/core/computation/parsing.py": 3084943521520476999, + "venv/lib/python3.13/site-packages/starlette/middleware/sessions.py": 11719848399661632707, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/dotenv.py": 5872848911499749500, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_index.py": 4908727359687606870, + "backend/git_repos/remote/test-repo.git/objects/52/ec4a78c629329b8b572ec9d5205271c111baac": 18054296544295201561, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_msvc.h": 17581467060937454503, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_conversion.pyi": 11580106670226366710, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_common.py": 2265736743367879051, + "docs/api/html/globals_n.html": 4026120167261781447, + "docs/api/html/group__test__list__tasks__success.html": 9673959434282998524, + "venv/lib/python3.13/site-packages/pygments/lexers/graphql.py": 2307854666015224677, + "docs/api/html/group__AuthService.html": 17583408280713591083, + "venv/lib/python3.13/site-packages/pygments/styles/lightbulb.py": 12433916113773598760, + "docs/api/html/group__test__orchestrator__start__session__preserves__partial__recovery.html": 7578527106229875658, + "docs/api/html/group__SupersetClientSanitizeUserText.html": 6776437542055180978, + "frontend/src/lib/ui/Card.svelte": 8508366526326051165, + "venv/lib/python3.13/site-packages/pygments/regexopt.py": 16073437303824961930, + "docs/api/html/group____build__chart__template__pool.html": 18077972997939014879, + "venv/lib/python3.13/site-packages/tzlocal/__init__.py": 11017072814026497364, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-1.csv": 16349400027062875699, + "venv/lib/python3.13/site-packages/keyring/core.py": 2764269590123131634, + "backend/src/plugins/translate/orchestrator_sql.py": 15420456250044832567, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/mean_.py": 3641008929910474457, + "docs/api/html/group__test__key__cols__none__returns__all.html": 8776324869922559549, + "venv/lib/python3.13/site-packages/jsonschema/tests/typing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/dateutil/tzwin.py": 5149446703746204349, + "docs/api/html/group__DashboardSchemas.html": 17302805929430757920, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py": 2866195356090164296, + "docs/api/html/group__test__validate__path__read__only__parent.html": 984350365930917404, + "docs/api/html/group____discover__route__permissions.html": 6994842999221083334, + "venv/lib/python3.13/site-packages/click/exceptions.py": 9734955259447933725, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_twodim_base.py": 17623133613018058380, + "docs/api/html/group____make__us3__session.html": 16077792546481746027, + "frontend/src/lib/auth/permissions.js": 7186663483939824478, + "docs/api/html/group__DocPreview.html": 10374942202918391563, + "docs/api/html/group__list__permissions.html": 2593836930320988861, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____isDashboardValidationBindingValid.html": 18348584692471214107, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/util.py": 1520899420719605380, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/exceptions.pyi": 9294219058621261971, + "backend/src/core/plugin_base.py": 5063738955101125652, + "venv/lib/python3.13/site-packages/numpy/_core/umath.py": 6132761780113682667, + "backend/src/plugins/translate/_run_source.py": 11569137727611732019, + "backend/git_repos/remote/test-repo.git/objects/8c/4ea7958b7a7e140d27e1e7d8d1f9912fdf5992": 5862166356451798197, + "docs/api/html/group__resolveDashboardRef__Function.html": 11580641610445169138, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_half.py": 3121836756584726126, + "docs/api/html/group__TestLoggingAuditFixes.html": 525186929866307297, + "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.pyi": 5879290708972838025, + "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.py": 6348811074249398741, + "venv/lib/python3.13/site-packages/PIL/_deprecate.py": 15076725072094847812, + "frontend/build/_app/immutable/chunks/DrjtL4d8.js": 6640873875065750959, + "docs/api/html/group__test__parse__datetime__iso__string.html": 8814123590453372630, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_arithmetic.py": 16829312896275893002, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_constructors.py": 9815164146739254076, + "backend/git_repos/remote/test-repo.git/hooks/pre-merge-commit.sample": 10013699873067956556, + "docs/api/html/bc_s.png": 11826155430197355290, + "venv/lib/python3.13/site-packages/pygments/lexers/_css_builtins.py": 17478340487186531268, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_astype.py": 11995587502880659624, + "backend/src/services/llm_prompt_templates.py": 16176053862101644239, + "docs/api/html/group____extract__dataset__review__target.html": 3215287800092748207, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/utils.py": 7707707044867513457, + "docs/api/html/group____normalize__lookup__payload.html": 11273830722028075585, + "docs/api/html/group__get__remote__id.html": 4917003252379423579, + "docs/api/html/group__get__logging__config.html": 13172275413110444262, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/getitem.py": 556339939822508169, + "venv/lib/python3.13/site-packages/pandas/_libs/indexing.pyi": 90995502095752726, + "venv/lib/python3.13/site-packages/pygments/lexers/bdd.py": 5389493859905397060, + "docs/api/html/group__test__inactive__schedule__skips.html": 17459537030862916433, + "venv/lib/python3.13/site-packages/anyio/abc/_sockets.py": 15042495744291042062, + "docs/api/html/group__ExtraFormDataMerge.html": 4791611138331694386, + "docs/api/html/group____make__task__manager.html": 5653521975879492956, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py": 11422235699126032386, + "venv/lib/python3.13/site-packages/numpy/ma/__init__.py": 214772578978182427, + "docs/api/html/group__delete__chart.html": 6831863026577067657, + "venv/lib/python3.13/site-packages/pygments/lexers/webidl.py": 8097260111959278117, + "venv/lib/python3.13/site-packages/ecdsa/test_curves.py": 6577762025900435885, + "venv/lib/python3.13/site-packages/numpy/ma/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_tzconversion.py": 7205566353454584259, + "venv/lib/python3.13/site-packages/pip/_internal/commands/configuration.py": 1608697090566485893, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg8000.py": 6580512354643849828, + "venv/lib/python3.13/site-packages/pygments/lexers/cddl.py": 1303059539145221713, + "venv/lib/python3.13/site-packages/_pytest/_io/wcwidth.py": 13515196388826152383, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.APACHE": 8359973023624924624, + "docs/api/html/group__test__ux__state__contracts.html": 17383839779640281908, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/casting.py": 12226803454582253097, + "docs/api/html/search/groups_c.js": 3976149661731985741, + "venv/lib/python3.13/site-packages/pip/_internal/models/installation_report.py": 14534544543677907640, + "docs/api/html/group__DashboardsApi.html": 8580056325448816200, + "docs/api/html/search/groups_18.js": 13110703267329058335, + "docs/api/html/group__getTaskLogs__Function.html": 4009919543108782960, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py": 3369246520501764730, + "frontend/build/_app/immutable/chunks/BHYxHk9m.js": 6074898573071417709, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_regression.py": 4611768764484593979, + "docs/api/html/group__ComplianceStageContext.html": 15065570275790964378, + "docs/api/html/group__init__db.html": 2707796602248353390, + "venv/lib/python3.13/site-packages/h11/_headers.py": 7592730288271984585, + "venv/lib/python3.13/site-packages/cffi/recompiler.py": 17809252277700094304, + "venv/lib/python3.13/site-packages/pygments/lexers/ruby.py": 6898093948224302592, + "backend/git_repos/remote/test-repo.git/objects/22/91d2eb86d77accf7401f8952778809cd1aa4d4": 5120428041118096941, + "docs/api/html/group__SessionCollaboratorRole.html": 3015568436480767444, + "docs/api/html/group__ComplianceExecutionService.html": 2017272940317881457, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_cov_corr.py": 13652339565475304679, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_both.f90": 13943066317159236257, + "docs/api/html/group____load__plugins.html": 2375668507770172038, + "docs/api/html/group__test__task__persistence.html": 1637708909770153120, + "venv/lib/python3.13/site-packages/cryptography/fernet.py": 10137792148038140630, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/METADATA": 8985010079096958084, + "venv/lib/python3.13/site-packages/numpy/polynomial/laguerre.pyi": 11310761736182213265, + "docs/api/html/minus.svg": 5110402206540728274, + "venv/lib/python3.13/site-packages/pygments/lexers/_julia_builtins.py": 8846111188795315103, + "frontend/e2e/tests/login.e2e.js": 9217667783118672119, + "venv/lib/python3.13/site-packages/rapidfuzz/utils.pyi": 7400725037332728015, + "venv/lib/python3.13/site-packages/pip/_internal/configuration.py": 2563100675379605810, + "venv/lib/python3.13/site-packages/pandas/core/roperator.py": 2587829042468916489, + "docs/api/html/group__DashboardHub__clearColumnFilter__Function.html": 11839612735463468101, + "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/RECORD": 8201759541483070364, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_transpose.py": 17983160984979323393, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.f": 9330535808093475892, + "venv/lib/python3.13/site-packages/authlib/common/security.py": 2291133227104016684, + "venv/lib/python3.13/site-packages/PIL/Jpeg2KImagePlugin.py": 16905402423248975204, + "docs/api/html/group__get__users__page.html": 9763104845120484996, + "docs/api/html/group__middleware__package.html": 12440510290894977924, + "docs/api/html/group__TestDeprecationAuditFixes.html": 9254534639082865942, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/__init__.py": 15644105878811738516, + "venv/lib/python3.13/site-packages/PIL/BmpImagePlugin.py": 16682567630621529903, + "docs/api/html/group__formatSize__Function.html": 8913344485781991642, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_between.py": 337429135216881486, + "docs/api/html/group__test__json__load__if__needed__none.html": 16725871875362821962, + "frontend/src/lib/auth/store.ts": 5831215136783173107, + "venv/lib/python3.13/site-packages/dateutil/__init__.py": 17666544837260663800, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/METADATA": 7727489762869155085, + "docs/api/html/search/all_13.js": 14323409538596817569, + "docs/api/html/group__SupersetClientExtractNativeFiltersFromPermalink.html": 11300039242994090403, + "docs/api/html/group__DashboardNotFoundError.html": 3334527199488317700, + "docs/api/html/group__Schedule.html": 13333689163022748798, + "docs/api/html/group__build__manifest.html": 14209312220395832967, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_categorical.py": 12897027063618569795, + "venv/lib/python3.13/site-packages/pydantic/v1/color.py": 14545509649623975912, + "venv/lib/python3.13/site-packages/_pytest/doctest.py": 6126919596776592278, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py": 6845813091876023610, + "venv/lib/python3.13/site-packages/pygments/styles/_mapping.py": 11752660241391232497, + "venv/lib/python3.13/site-packages/cffi/__init__.py": 13427266446746498548, + "backend/src/services/clean_release/__init__.py": 9144880924791476391, + "docs/api/html/group__InitAuthDbScript.html": 8842607924154924200, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_loongarch64_linux.h": 6566098590441297326, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2py2e.py": 8571347023940385644, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_format.py": 1947585389944838076, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_reductions.py": 2816859231048302619, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA": 7788873460115135563, + "venv/lib/python3.13/site-packages/idna/compat.py": 9758194415584776667, + "backend/src/services/clean_release/stages/__init__.py": 2966227562547886302, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_inference.py": 16402427186697071465, + "venv/lib/python3.13/site-packages/pandas/core/config_init.py": 16537122488227845090, + "docs/api/html/group__DashboardHub__toggleFilterValue__Function.html": 4775698047438821697, + "docs/api/html/group__test__get__task__log__stats__success.html": 17411659170669298649, + "docs/api/html/group__ReleaseCandidate.html": 17443285489751037523, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_insert.py": 6970975909565157982, + "docs/api/html/group__update__provider.html": 11827750116531988760, + "docs/api/html/group__build__parser.html": 9963847217279686225, + "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.py": 2490579175967301457, + "venv/lib/python3.13/site-packages/numpy/f2py/f2py2e.pyi": 14181518821845356636, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_windows.py": 9171347078900057357, + "venv/lib/python3.13/site-packages/gitdb/db/loose.py": 17467413653016810702, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/numpy_/test_indexing.py": 13703573544185051910, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/rsa_key.py": 7092106298733667895, + "docs/api/html/group__test__build__dataset__preview__query__context__places__recovered__filters__in__chart__style__form__data.html": 18337321725983567020, + "venv/lib/python3.13/site-packages/secretstorage/defines.py": 9590959702075554048, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_kwarg.py": 10417724544218303733, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/infer.py": 12541904372617915365, + "docs/api/html/group__test__key__without__end__time.html": 1016524169546718308, + "docs/api/html/group____parse__command.html": 2613491451304576155, + "venv/lib/python3.13/site-packages/cffi/lock.py": 13507686939203117974, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_quoted_character.py": 2316632434571797367, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_categorical.py": 9615571808720893016, + "backend/src/core/auth/jwt.py": 17234322122501341628, + "docs/api/html/group__test__get__datasets__not__found.html": 12470922355054560217, + "venv/lib/python3.13/site-packages/PIL/MpoImagePlugin.py": 644327650732339301, + "venv/lib/python3.13/site-packages/authlib/oidc/core/__init__.py": 16029653645350087968, + "docs/api/html/group__getTaskStatus__Function.html": 16832723243113851896, + "docs/api/html/group__DbMock.html": 17152034194564155607, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertsql.py": 4633484475514351889, + "docs/api/html/group__SupersetClientGetDashboards.html": 13070946675530774203, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cos.csv": 12896264205153363031, + "docs/api/html/globals_i.html": 10148981415694925638, + "docs/api/html/group__LLMProvider.html": 9637931473416605279, + "frontend/src/lib/components/StartMaintenanceForm.svelte": 11114259982483681666, + "venv/lib/python3.13/site-packages/pandas/_libs/hashing.cpython-313-x86_64-linux-gnu.so": 16124435225170610034, + "venv/lib/python3.13/site-packages/pip/_internal/models/selection_prefs.py": 14700113736620314923, + "docs/api/html/group__TestStorageAuditFixes.html": 9774243051070118436, + "venv/lib/python3.13/site-packages/PIL/ImageCms.py": 5610897002957128546, + "docs/api/html/group__ComplianceExecutionResult.html": 11417659871930501440, + "docs/api/html/search/functions_0.js": 18275711831096102872, + "docs/api/html/group__openEditModal__Function.html": 6284886960680022829, + "docs/api/html/group__resolve__merge__conflicts.html": 237817549844254377, + "venv/lib/python3.13/site-packages/websockets/frames.py": 4901850477414613953, + "docs/api/html/group__test__revoke__unknown__publication__rejected.html": 2648436309555498591, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_bar.py": 7791068963246005463, + "venv/lib/python3.13/site-packages/_pytest/junitxml.py": 5871715626272366062, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____startMaintenance.html": 15248104502781351783, + "docs/api/html/group__CleanReleaseV2ApiTests.html": 17398513879675376238, + "docs/api/html/group__AppDependencies.html": 8553210380267321705, + "docs/api/html/group__validate__path.html": 9061978304641306658, + "venv/lib/python3.13/site-packages/cffi/backend_ctypes.py": 2042566833861899211, + "frontend/src/lib/i18n/index.ts": 14616557360624297847, + "venv/lib/python3.13/site-packages/pandas/core/arrays/timedeltas.py": 7318851108932357510, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_fiscal.py": 14652343698825214870, + "venv/lib/python3.13/site-packages/rapidfuzz/py.typed": 15130871412783076140, + "backend/src/plugins/translate/__tests__/test_token_budget.py": 2275341408164077909, + "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet.py": 10727595500186011102, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_argsort.py": 7320661835159648143, + "docs/api/html/group__localize.html": 16532518584734972010, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_searchsorted.py": 12074107109575676897, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.pyi": 13173581775000480811, + "venv/lib/python3.13/site-packages/pycparser/c_generator.py": 14922277346364091122, + "docs/api/html/group__test__multiple__tables.html": 3501237513508769585, + "venv/lib/python3.13/site-packages/click/_compat.py": 14451519072991457993, + "docs/api/html/group__DatasetReviewProfileModels.html": 9977462433342924108, + "docs/api/html/search/functions_e.js": 16386749146387179095, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/__init__.py": 1923528231486706442, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py": 5630511062945201327, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_month.py": 1613806538807726074, + "frontend/src/routes/tools/backups/+page.svelte": 2765328366215862593, + "docs/api/html/group__ScheduleConfig.html": 11162916934238527143, + "venv/lib/python3.13/site-packages/pygments/styles/perldoc.py": 4857683935715743356, + "venv/lib/python3.13/site-packages/httpcore/_api.py": 16499985295426060327, + "venv/lib/python3.13/site-packages/attr/_cmp.py": 3297594878548179300, + "venv/lib/python3.13/site-packages/passlib/utils/handlers.py": 10182859005870689026, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/WHEEL": 11630459739183915695, + "docs/api/html/group__test__html__only__content.html": 3771397076204846018, + "docs/api/html/group__CleanReleaseContracts.html": 13165007053860558990, + "docs/api/html/group____resolve__markdown__datasource.html": 15564490152308108829, + "docs/api/html/group__test__remove__nonexistent__translation__job__no__error.html": 5929457228764208486, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE": 2345070715845618109, + "venv/lib/python3.13/site-packages/_pytest/tracemalloc.py": 5530234062530858720, + "venv/lib/python3.13/site-packages/pip/_internal/utils/retry.py": 15244280495983216204, + "docs/api/html/group__test__cli__compliance__run__scaffold.html": 11653178433842340027, + "docs/api/html/group__SupersetAccountLookupAdapter.html": 16273317223969819045, + "docs/api/html/group__TestDryRunOrchestrator.html": 11981206571409362239, + "docs/api/html/group__test__error__string__fallback.html": 16246095716833508917, + "venv/lib/python3.13/site-packages/_pytest/deprecated.py": 5452408732344157294, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_getlimits.py": 118041882543413669, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.py": 17817123894903120316, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/size/foo.f90": 18067977329435720684, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_strptime.py": 14328510310436552635, + "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django_source.py": 15577951591316518967, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_numeric.py": 9383693732009547935, + "venv/lib/python3.13/site-packages/pandas/_testing/_hypothesis.py": 12298650110193262367, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mutable.py": 9323088483998280629, + "venv/lib/python3.13/site-packages/dateutil/tz/win.py": 8427821225251397364, + "docs/api/html/group__invalidate__timezone__cache.html": 10993258758120512207, + "venv/lib/python3.13/site-packages/sqlalchemy/util/concurrency.py": 1325673503812009695, + "frontend/build/_app/immutable/chunks/T8YAys4l.js": 6993064462971658251, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_math.h": 16701045088211015518, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/REQUESTED": 15130871412783076140, + "backend/src/api/routes/translate/__init__.py": 6357999798616465776, + "docs/api/html/group____encode__sql__value.html": 14512027224697252876, + "docs/api/html/search/mag.svg": 12711116757118699539, + "venv/lib/python3.13/site-packages/numpy/_core/shape_base.py": 3996519099530770308, + "docs/api/html/group__test__storage__config__fields__have__correct__types.html": 13803318122009828230, + "docs/api/html/group__StoresMock.html": 2868613847587767061, + "docs/api/html/group____matches__actor__case__insensitive.html": 218018109702683196, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_null_file.py": 6885565068723353414, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/style.py": 12654125988995362593, + "docs/api/html/group__AuthStore.html": 6433208268652425169, + "docs/api/html/group____persist__message.html": 7636295246273294885, + "venv/lib/python3.13/site-packages/pandas/tests/api/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_arithmetics.py": 16527169124402715210, + "frontend/build/_app/immutable/chunks/l4w9mDu3.js": 11983073101291382930, + "docs/api/html/group__decode__token.html": 4126673060024098894, + "frontend/build/_app/immutable/chunks/BIfxqBVb.js": 1251132010861554320, + "backend/src/api/auth.py": 14299178372526268610, + "docs/api/html/group__test__get__remote__id__returns__integer.html": 10764704660366053638, + "docs/api/html/group__test__get__run__detail.html": 4912801148491147951, + "venv/lib/python3.13/site-packages/click/globals.py": 18353671282454331523, + "frontend/src/components/MissingMappingModal.svelte": 14222261650100098814, + "backend/git_repos/remote/test-repo.git/objects/17/66251d8cb2693136e09ea3def3e776559cb722": 1137762010493457272, + "docs/api/html/search/search.css": 7835737499276132321, + "docs/api/html/doxygen__docs_8h.html": 4085601024655800433, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/numpyconfig.h": 13339221870252345205, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape_base.pyi": 10291383658131961346, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_infer_objects.py": 6769980076977692890, + "docs/api/html/group__test__extract__native__filters__from__key.html": 3900857029224500253, + "frontend/build/_app/immutable/nodes/40.vrrWtLk5.js": 10294241495763253835, + "backend/tests/test_auth.py": 11969794167830099791, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rank.py": 5817408252050193538, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_ewm.py": 13789842684407148830, + "docs/api/html/group__clean__text.html": 5688432700406149182, + "venv/lib/python3.13/site-packages/pip/_internal/utils/compat.py": 1841193561752758336, + "docs/api/html/group____flush__task__logs.html": 9037017981358195543, + "backend/tests/test_security_audit_fixes.py": 13632958589377478866, + "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.py": 12355826783958402233, + "docs/api/html/group__TranslateHistoryTests.html": 16053454222263447135, + "venv/lib/python3.13/site-packages/greenlet/CObjects.cpp": 3454797518210223926, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/__init__.py": 9884041726796476720, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/client_mixin.py": 17980753662107479779, + "venv/lib/python3.13/site-packages/pygments/lexers/_lasso_builtins.py": 4006621424665751347, + "docs/api/html/group__PLUGIN__TO__TASK__TYPE.html": 17348671743390700348, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/generate_umath_validation_data.cpp": 10880681943327843928, + "docs/api/html/group__belief__scope__Function.html": 12952448433922544231, + "docs/api/html/group__handleBulkSync__Function.html": 8500634733610681572, + "venv/lib/python3.13/site-packages/click/decorators.py": 2920877667855377423, + "venv/lib/python3.13/site-packages/pygments/lexers/macaulay2.py": 8022891778577314212, + "frontend/src/lib/stores.js": 8225317698711142177, + "venv/lib/python3.13/site-packages/fastapi/dependencies/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/common.py": 11384497819737275399, + "docs/api/html/group__DatasetReviewApi.html": 5473514434428079682, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/__init__.py": 8558541715982163200, + "venv/lib/python3.13/site-packages/numpy/lib/_datasource.py": 18136853038949826147, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_spss.py": 4865360315909532603, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/pyproject.py": 12684373765476932108, + "venv/lib/python3.13/site-packages/referencing/_attrs.pyi": 10030665705880994025, + "docs/api/html/group__TranslationBatch.html": 8584550832559177298, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_py.py": 13809503317864539103, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/compat.py": 9758194415584776667, + "venv/lib/python3.13/site-packages/dateutil/tz/_common.py": 9009072600471223066, + "backend/tests/test_translate_scheduler_execution.py": 13388419486236314627, + "docs/api/html/group__build__default__stages.html": 1764973272728108457, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/generic.py": 17755579497690180620, + "docs/api/html/group__PreviewCompilationPayload.html": 10856751322722254656, + "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/algorithms.py": 2375452080667815941, + "docs/api/html/group__EnvironmentMock.html": 17907946510124954226, + "docs/api/html/group__test__missing__task__id.html": 366377427514445618, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/sqlalchemy.py": 811968833735951956, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/matrix.pyi": 15667024840812783911, + "venv/lib/python3.13/site-packages/jeepney/io/__init__.py": 2568357146184259175, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23533.f": 12558972205518833024, + "docs/api/html/group__RevokeRequest.html": 3725473471343488816, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_coercion.py": 7261787308019145312, + "docs/api/html/group__test__compliant__run__validation.html": 7010442946526003143, + "venv/lib/python3.13/site-packages/pydantic/_internal/_mock_val_ser.py": 12265320534388472158, + "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.py": 14446508217220482719, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_string_array.py": 8260883866404398980, + "docs/api/html/group__AuthSessionLocal.html": 12226538716122831966, + "frontend/src/lib/components/layout/__tests__/test_topNavbar.svelte.js": 1873684170678703500, + "docs/api/html/group__db__session.html": 16354950519275316694, + "docs/api/html/group__build__partial__recovery__findings.html": 14829932052677515671, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-2.csv": 14641856424618845798, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.pyi": 13208953770241664384, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odswriter.py": 5134385709728943904, + "docs/api/html/group____patch__dashboard__metadata.html": 4454475099365896302, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/windows.py": 16452996247375488034, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.pyi": 15864712173815045309, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/astype_copy.pkl": 4470155972891419163, + "venv/lib/python3.13/site-packages/pandas/core/frame.py": 2630807744775247533, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategies.py": 15673926142454922581, + "docs/api/html/group__update__maintenance__settings.html": 17618732781129600578, + "docs/api/html/group__SupersetContextExtractorBase.html": 14341165499851987732, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_shift.py": 8547819448301023303, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_truncate.py": 6478126371906156409, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.py": 2401931196871484449, + "venv/lib/python3.13/site-packages/pygments/formatters/other.py": 13124903290103898118, + "backend/alembic/versions/a7b1c2d3e4f5_add_provider_id_to_validation_policies.py": 887370167154457158, + "venv/lib/python3.13/site-packages/smmap/__init__.py": 4557520096828426601, + "backend/src/core/task_manager/event_bus.py": 1436794780600195410, + "venv/lib/python3.13/site-packages/psycopg2/_range.py": 7184417109068014771, + "docs/api/html/group__push__changes.html": 8985338630049484293, + "venv/lib/python3.13/site-packages/numpy/ma/core.pyi": 4829891173366130328, + "venv/lib/python3.13/site-packages/fastapi/concurrency.py": 17937688838625807004, + "venv/lib/python3.13/site-packages/pygments/lexers/console.py": 18266795648198475028, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__loadEnvironments__Function.html": 5584041378316217605, + "docs/api/html/group__TranslationRunResponse.html": 11155567943243725782, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_misc.py": 9190561773853901527, + "venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py": 10408538054406276433, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/base.py": 10514193453447954780, + "docs/api/html/group__test__get__report__success.html": 13133681146386093411, + "docs/api/html/group__run__init.html": 4726084133334129935, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py": 14768815444701392013, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_constructors.pyi": 4993066609272697419, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_indexing.py": 9862204223281834360, + "docs/api/html/group__DEFAULT__EXECUTION__PROMPT__TEMPLATE.html": 4059085208364710234, + "venv/lib/python3.13/site-packages/anyio/_backends/_trio.py": 4764555436057754985, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get.py": 6509102681661636928, + "docs/api/html/group__Card.html": 8557385791478592213, + "docs/api/html/group__test__end__all__returns__202.html": 2143037192454601486, + "docs/api/html/search/functions_9.js": 6423268791923178074, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.cpython-313-x86_64-linux-gnu.so": 5441755022198996487, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/core": 10616484054735611337, + "venv/lib/python3.13/site-packages/pandas/core/interchange/buffer.py": 12961802206383700922, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/parameters.py": 247312908510013930, + "backend/alembic/versions/9f8e7d6c5b4a_add_is_multimodal_to_llm_providers.py": 909967615686736061, + "venv/lib/python3.13/site-packages/pandas/tests/resample/conftest.py": 1643522609234951699, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_longdouble.py": 9337218715436213122, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_numba.py": 6409810497251251179, + "venv/lib/python3.13/site-packages/urllib3/_base_connection.py": 14495079097019980154, + "venv/lib/python3.13/site-packages/pydantic/v1/generics.py": 1150594223414883339, + "docs/api/html/group__approve__candidate.html": 16919945520220435820, + "docs/api/html/group____seed__with__candidate__policy__registry.html": 2918236783591851190, + "venv/lib/python3.13/site-packages/PIL/ImagePalette.py": 9227837918026991107, + "docs/api/html/group__normalize__whitespace.html": 7327563330573824911, + "venv/lib/python3.13/site-packages/ecdsa/test_sha3.py": 12459459298663975730, + "venv/lib/python3.13/site-packages/pygments/styles/algol.py": 3040373291664740876, + "docs/api/html/group__OUTPUT__PER__ROW__PER__LANG.html": 8649435613507294987, + "docs/api/html/pages.html": 4338336449088201146, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/AUTHORS": 17022185907188244883, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/meson.build.template": 9935732744553086652, + "docs/api/html/search/files_0.js": 1537156212390677004, + "venv/lib/python3.13/site-packages/attrs/__init__.py": 9502968706258875910, + "venv/lib/python3.13/site-packages/pygments/lexers/apdlexer.py": 14222752852221009085, + "backend/tests/test_alembic_migrations.py": 9919138859926526277, + "dist/docker/ss-tools-enterprise-0.1.5.bundle": 1211304723546208185, + "docs/api/html/group__add__task.html": 557592669952894708, + "docs/api/html/group__preview__session__serializer.html": 5685665567268715831, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arithmetic.pyi": 10265823318697442495, + "docs/api/html/group__ReactiveDashboardFetch__Block.html": 2051150487389286262, + "docs/api/html/group__get__batches.html": 597462312194719420, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE": 3996324383113221529, + "docs/api/html/group__TranslationPreviewRecord.html": 12396412092000079349, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/idnadata.py": 1169745920682458213, + "docs/api/html/group__test__parse__datetime__integer.html": 1059776138445462771, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pycparser/plyparser.py": 5251740466238035035, + "venv/lib/python3.13/site-packages/numpy/rec/__init__.py": 7467092880535460658, + "docs/api/html/group__ManifestSummary.html": 5053023856336130679, + "venv/lib/python3.13/site-packages/passlib/handlers/sha1_crypt.py": 5752556581809938954, + "docs/api/html/group__clean__release__facade.html": 6575150253388246217, + "docs/api/html/group__DictionaryEntryResponse.html": 14840442294467132189, + "docs/api/html/group__StaticFiles.html": 11218723552719220007, + "venv/lib/python3.13/site-packages/pandas/core/computation/scope.py": 16859504937881548797, + "docs/api/html/group__MappingStatus.html": 18314626760542529454, + "docs/api/html/group__RunDetailResponse.html": 6291679337051583792, + "docs/api/html/group__TaskHistory.html": 12837645096150183863, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/WHEEL": 17410883677306885019, + "docs/api/html/group__normalizeEnvStage__Function.html": 702224648569356204, + "docs/api/html/group__TranslationResultAggregator.html": 7516401936647746773, + "venv/lib/python3.13/site-packages/yaml/representer.py": 10550136298978225013, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/token.py": 6987328478153445584, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_abc.py": 10164280045317807365, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/__init__.py": 15130871412783076140, + "docs/api/html/group__TranslationRunGlobalIndicator.html": 7439172912927142076, + "venv/lib/python3.13/site-packages/jose/backends/native.py": 50023158618000658, + "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimelike.py": 394614958716301173, + "docs/api/html/group____persist__confirmation.html": 1641246192766111863, + "docs/api/html/group__frontend__src__services__adminService__js____createADGroupMapping.html": 543017303917156388, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_values.py": 8372572954663628844, + "backend/src/api/routes/maintenance/__init__.py": 8921716690761316245, + "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.pyi": 14069586330277925998, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_array.py": 11271807719833350332, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/subcomponents.py": 10365594934527289233, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__DatasetHub.html": 15956774378542048676, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/__init__.py": 6532094538918910232, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npy": 17303060773267383537, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_timedelta64.py": 16821719402662529276, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_setops.py": 5208912445718807372, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_join.py": 4213933887062089605, + "backend/src/core/utils/superset_context_extractor/_parsing.py": 18406152325228380468, + "backend/src/api/routes/assistant/_admin_routes.py": 12159303428660924995, + "backend/src/services/dataset_review/__init__.py": 3695054675894579272, + "venv/lib/python3.13/site-packages/pygments/lexers/promql.py": 8929925488296216854, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/fixed_string.f90": 8654788371538544793, + "backend/src/services/git/_remote_providers.py": 4516794944352111126, + "docs/api/html/search/functions_16.js": 14777540345560864351, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/panel.py": 17832701246248000383, + "venv/lib/python3.13/site-packages/numpy/tests/test_configtool.py": 8208415015478205502, + "docs/api/html/group__test__normalizeTranslateError__Function.html": 11445053005894736515, + "docs/api/html/group____discover__route__permissions__cached.html": 18354861251821323391, + "docs/api/html/group__createUser__Function.html": 9916552781618582040, + "docs/api/html/group__SupersetAccountCandidate.html": 12015919113929683883, + "venv/lib/python3.13/site-packages/attr/converters.pyi": 17675091788614033166, + "docs/api/html/group__setAssistantFocusTarget__Function.html": 13485754571727078546, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py": 6109201994963211857, + "docs/api/html/group__test__api__create__job.html": 15555049302714485159, + "venv/lib/python3.13/site-packages/psycopg2/pool.py": 8855184580138330938, + "docs/api/html/group__test__plugin__loader__uses__logger__not__print.html": 9927001174578654853, + "backend/src/core/superset_client/_dashboards_write.py": 17768282769896196778, + "venv/lib/python3.13/site-packages/rsa/core.py": 13203947234024924497, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_setops.py": 14246225281428284850, + "venv/lib/python3.13/site-packages/pygments/lexers/scripting.py": 4167168074165473790, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_index_new.py": 796607958391882182, + "docs/api/html/navtreeindex9.js": 16489008554418984705, + "backend/src/plugins/translate/__tests__/test_preview.py": 13201708414970983394, + "docs/api/html/group__ReviewWorkspaceRightRail.html": 6482365234920032721, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_day.py": 2200008998040663785, + "docs/api/html/group__clean__release__dto.html": 8303338663879364604, + "docs/api/html/group__test__enterprise__policy__valid.html": 195059262392650565, + "docs/api/html/group__Token.html": 5997049889797174101, + "docs/api/html/group__get__plugin__loader.html": 13012257189536947305, + "venv/lib/python3.13/site-packages/greenlet/TStackState.cpp": 16720079760074325527, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2": 6199424505696679197, + "venv/lib/python3.13/site-packages/authlib/oauth2/base.py": 12948318231066662045, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/structs.py": 4479429145468072604, + "docs/api/html/group__GitLLMExtensionModule.html": 5836066463515389762, + "docs/api/html/group__DatasetReviewSchemas.html": 5120136138050590201, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/__init__.py": 12793458753561114666, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_one_public_routine.f90": 7957683453180939605, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_missing.py": 7034102519042351965, + "backend/alembic/README": 17625003422504608299, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dim2.py": 5767600151459095125, + "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5_dep.dot": 17431168212830508462, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8693/__init__.py": 11065844267208884531, + "venv/lib/python3.13/site-packages/numpy/_core/_methods.py": 16805668769357938792, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_shape_base.py": 4117141349022217163, + "docs/api/html/group__setUser__Function.html": 17905907968752822833, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayscalars.h": 16142050006935459679, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/base.cpython-313-x86_64-linux-gnu.so": 8335798324308957127, + "docs/api/html/group____filters__imports.html": 16499647191503680899, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/base.py": 3628510830969664111, + "docs/api/html/group__LiveProjectCheckE2E.html": 17814476116211572824, + "docs/api/html/group____generate__insert__sql.html": 5361897751695942720, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log2.csv": 13979868713429167140, + "venv/lib/python3.13/site-packages/charset_normalizer/__main__.py": 8887772289570078278, + "docs/api/html/group__test__get__task__success.html": 10530613508507125281, + "venv/lib/python3.13/site-packages/pandas/io/excel/_base.py": 1251545894005373734, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getWorkspaceStateLabel.html": 3907642846891320094, + "venv/lib/python3.13/site-packages/numpy/ma/core.py": 338350463523585845, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension.c": 9318655518998644962, + "venv/lib/python3.13/site-packages/pygments/lexers/dalvik.py": 5549983892200382076, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____patchSession.html": 5561812461916057904, + "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__id__from__state.html": 2544335115240783642, + "venv/lib/python3.13/site-packages/pip/_internal/wheel_builder.py": 3893922370396791826, + "docs/api/html/group__MaintenanceSchemasModule.html": 9161032253446542473, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ed448.py": 12264234876933578367, + "docs/api/html/group__FilterStateModels.html": 10931585648189564325, + "docs/api/html/group__resolve__trusted__policy__snapshots.html": 1966325452563273431, + "frontend/src/routes/+layout.svelte": 2957369003695133671, + "venv/lib/python3.13/site-packages/pandas/_libs/testing.pyi": 16691791330088523786, + "docs/api/html/group__test__parse__dashboard__url__for__filters__permalink.html": 5330991424170288666, + "venv/lib/python3.13/site-packages/authlib/common/urls.py": 13627272625165922482, + "venv/lib/python3.13/site-packages/pandas/core/_numba/__init__.py": 15130871412783076140, + "docs/api/html/group____get__latest__clarification__session__or__404.html": 13364115311427816838, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_shape_base.py": 8837845103338426570, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_iteration.py": 2118501258411211851, + "venv/lib/python3.13/site-packages/cryptography/x509/general_name.py": 4481582226129488535, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/ma/LICENSE": 6723325434476453127, + "docs/api/html/group____mock__env.html": 6811360979523770361, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____startPolling.html": 5414561271979953996, + "venv/lib/python3.13/site-packages/numpy/_core/_internal.pyi": 6840742333427164920, + "docs/api/html/group__TranslateApiTests__Class.html": 5018753849981248243, + "venv/lib/python3.13/site-packages/pandas/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/fastapi/openapi/models.py": 1680354178147364813, + "venv/lib/python3.13/site-packages/pandas/plotting/_core.py": 6766059152026241747, + "docs/api/html/group__retry__insert.html": 11772440759533263654, + "venv/lib/python3.13/site-packages/pip/_internal/cli/parser.py": 1064209424435927674, + "docs/api/html/group____build__sql__lab__redirect__url.html": 4568202178269138535, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/METADATA": 11419135077900073883, + "docs/api/html/group__frontend__src__services__adminService__js____updateUser.html": 10078748986285317841, + "frontend/build/_app/immutable/nodes/32.DldV82Pn.js": 13256726079308868441, + "backend/tests/test_translate_jobs.py": 4194751947248569697, + "backend/src/plugins/translate/__tests__/test_inline_correction.py": 10762757907935213454, + "frontend/src/routes/profile/+page.svelte": 13080303244628934788, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/client.py": 18443944554871937480, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_algorithms.py": 15219408648894515493, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_records.py": 18167578114159312327, + "backend/src/services/clean_release/policy_resolution_service.py": 1025435122296379201, + "docs/api/html/group__TranslatePlugin.html": 3645253746992049971, + "docs/api/html/group__uploadFile__Function.html": 2179256576386234524, + "docs/api/html/group__MaintenanceChartManager.html": 457454489502690100, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/__init__.py": 96341051676048910, + "venv/lib/python3.13/site-packages/dotenv/py.typed": 9796674040111366709, + "frontend/src/lib/components/layout/TaskDrawer.svelte": 2803962847278233075, + "docs/api/html/group__DashboardsRouter.html": 16823726392565419686, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/quantile.py": 17341029201525362074, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_datetime.py": 1678885152993000592, + "docs/api/html/group__GitManager.html": 13687479863451466340, + "docs/api/html/group__ReportDetailUxTest__Module.html": 17811262593949560202, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/wheel.py": 7728646523045538484, + "venv/lib/python3.13/site-packages/apscheduler/triggers/base.py": 1170652622104554892, + "backend/src/scripts/clean_release_cli.py": 777127038463156812, + "frontend/e2e/tests/enterprise-clean-setup.e2e.js": 6434855878266981931, + "docs/api/html/group__test__get__logs__with__pagination.html": 2508602189967508929, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/request.py": 10492090878715126550, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____clearOnCompleteCallback.html": 13631531026182073774, + "docs/api/html/group__AsyncSupersetClientInit.html": 2528317931875935210, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/cmac.py": 12011944740750051829, + "docs/api/html/group__is__multimodal__model.html": 3951793497188229823, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/util.py": 387985965709546512, + "venv/lib/python3.13/site-packages/yaml/__init__.py": 11044333164208875121, + "venv/lib/python3.13/site-packages/pygments/styles/murphy.py": 8177992406082781615, + "venv/lib/python3.13/site-packages/pygments/lexers/bqn.py": 11823648528816858738, + "docs/api/html/group__run__candidate__register.html": 2561393854945297828, + "docs/api/html/group__client.html": 9166531319498298577, + "venv/lib/python3.13/site-packages/pip/_internal/models/pylock.py": 16697828625021519459, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_indexing.py": 4334578480714520407, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/types.py": 16795741817129898714, + "docs/api/html/group__TokenData.html": 10513747130917299744, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/use_data.f90": 14624074967635860696, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_at.py": 1029744344133153888, + "docs/api/html/group__build__columns.html": 14782239725954054542, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/scanner.py": 5640381644804445932, + "docs/api/html/group__LLMAnalysisPackage.html": 5388615837684236664, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group____normalize__timestamp__value.html": 16608102179483469578, + "venv/lib/python3.13/site-packages/pandas/_libs/json.cpython-313-x86_64-linux-gnu.so": 2436285663630821963, + "docs/api/html/group__test__get__datasets__service__failure.html": 7841926539827711165, + "venv/lib/python3.13/site-packages/pydantic/alias_generators.py": 5991580234179623842, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/endpoints.py": 18334891787808122725, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_cat_accessor.py": 4304062895283374927, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_explode.py": 18263694040596159591, + "venv/lib/python3.13/site-packages/pandas/core/internals/managers.py": 13547912653290063698, + "venv/lib/python3.13/site-packages/httpcore/_sync/http_proxy.py": 14037211594707072958, + "venv/lib/python3.13/site-packages/pygments/token.py": 6987328478153445584, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/WHEEL": 5179340427739743287, + "docs/api/html/group__DashboardTaskHistoryItem.html": 1713647374003736687, + "docs/api/html/group__FetchModelsRequest.html": 12761345404730870015, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_grouping.py": 5994909693214299971, + "venv/lib/python3.13/site-packages/pygments/formatters/html.py": 1918541918263449443, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.cpython-313-x86_64-linux-gnu.so": 12654188486128938170, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_openid.py": 8708980645667847439, + "venv/lib/python3.13/site-packages/referencing/typing.py": 8296636464219681747, + "docs/api/html/group__frontend__src__services__toolsService__js____runTask.html": 8322693038275083284, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/__init__.py": 15130871412783076140, + "docs/api/html/group__test__create__missing__name.html": 13297392854389513523, + "docs/api/html/group__test__duplicate__job__custom__name.html": 1186685615290746284, + "venv/lib/python3.13/site-packages/pygments/lexers/igor.py": 6506221273085723501, + "docs/api/html/group__test__update__job.html": 13489385965364167749, + "docs/api/html/group__ColumnDescriptionUpdate.html": 13860285606848382812, + "venv/lib/python3.13/site-packages/fastapi/datastructures.py": 727178955746650271, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/METADATA": 12747626683405487434, + "docs/api/html/group__AssistantChatStore.html": 13379737640010123041, + "venv/lib/python3.13/site-packages/attr/converters.py": 5179615793507170642, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/attr/__init__.pyi": 13203080513025719049, + "backend/src/api/routes/__tests__/test_git_status_route.py": 4501452711469218555, + "venv/lib/python3.13/site-packages/pygments/styles/colorful.py": 15708848112363953394, + "docs/api/html/group__SupersetClientValidateExportResponse.html": 2041159810613362188, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_util.py": 16908393806993809474, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__test__persist__task__failed__extracts__error.html": 3464011014621751693, + "docs/api/html/group__ReportStatus.html": 11659907177460625683, + "docs/api/html/group__TestConfigManagerCompat.html": 13874940420207193288, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/wrapper.py": 14599195010683170123, + "venv/lib/python3.13/site-packages/pandas/core/arrays/string_.py": 14865733321317939478, + "docs/api/html/group__test__sync__environment__deletes__stale__mappings.html": 11101146135939187286, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_sh_gcc.h": 3574353221233465215, + "docs/api/html/group__DashboardsResponse.html": 9460608762964369730, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/resource_protector.py": 11539718751174980004, + "docs/api/html/group__resolve__namespace.html": 18066940045737474091, + "docs/api/html/group__TranslationPreviewLanguage.html": 13625916657703783936, + "docs/api/html/group__profile__preference__service.html": 11536607414856501254, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py": 9964642152916165317, + "docs/api/html/group__TestCleanReleaseModels.html": 7307930441105961889, + "docs/api/html/group__handleTest__Function.html": 17441043050901879377, + "docs/api/html/group__SupersetClientImportDashboard.html": 8056171795857604639, + "frontend/src/routes/datasets/DatasetPreview.svelte": 4250995999947659034, + "docs/api/html/group____trigger__backup.html": 17397691449018850685, + "venv/lib/python3.13/site-packages/numpy.libs/libquadmath-96973f99-934c22de.so.0.0.0": 5098713761946118139, + "docs/api/html/group__preview__session__ops.html": 10729427969834726798, + "backend/src/core/plugin_loader.py": 8193009851819399597, + "frontend/src/lib/stores/__tests__/test_datasetReviewSession.js": 8646274524484627210, + "venv/lib/python3.13/site-packages/numpy/py.typed": 15130871412783076140, + "docs/api/html/group____resolve__dashboard__title.html": 7648595970899356173, + "frontend/src/routes/datasets/__tests__/DatasetList.test.js": 10189122239208504978, + "venv/lib/python3.13/site-packages/pip/_internal/index/sources.py": 11609272011565024668, + "backend/git_repos/remote/test-repo.git/objects/ed/0432f88f5f7d95b26ecde34345abe741dd9db2": 13426916511127204463, + "docs/api/html/group__test__audit__hooks__emit__append__only__event__stream.html": 17318960105938087804, + "docs/api/html/group__ActivityStore.html": 16046706190501806767, + "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.py": 2953876934840155920, + "docs/api/html/group__update__environment.html": 9173380704665104139, + "docs/api/html/group__SidebarStore.html": 9185421740800209773, + "venv/lib/python3.13/site-packages/pip/_internal/models/wheel.py": 10473287225032326206, + "venv/lib/python3.13/site-packages/_pytest/warning_types.py": 10695097290648045427, + "venv/lib/python3.13/site-packages/pydantic/v1/networks.py": 13262073155118055511, + "docker/all-in-one.Dockerfile": 10896793266219904099, + "venv/lib/python3.13/site-packages/pygments/lexers/comal.py": 769727871242464458, + "venv/lib/python3.13/site-packages/rsa/transform.py": 12261738253972114335, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/METADATA": 13633829318759752611, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_read_errors.py": 4559483361564472117, + "venv/lib/python3.13/site-packages/jeepney/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/__init__.py": 308178910015974550, + "venv/lib/python3.13/site-packages/pygments/lexers/apl.py": 16767474673619335683, + "docs/api/html/group__MaintenanceDashboardBannerStatus.html": 4467400780797905740, + "docs/api/html/group__SupersetClient.html": 17296693003009978086, + "frontend/src/components/git/__tests__/git_manager.unfinished_merge.integration.test.js": 14696146969807239134, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py": 5895297076117159245, + "docs/api/html/group__create__ad__mapping.html": 5919069328535603221, + "docs/api/html/group__TranslatePluginPackage.html": 16341267951062510196, + "docs/api/html/group__MarkerLogger__reflect.html": 9704040044531782477, + "docs/api/html/group__test__schedule__not__found__skips.html": 10866822650203142876, + "venv/lib/python3.13/site-packages/python_multipart/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_mixed.py": 3476101699679108025, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_asof.py": 18383318159051131669, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/string.tpl": 15796203549130590784, + "docs/api/html/group__get__repository__status.html": 2242859351176196482, + "venv/lib/python3.13/site-packages/jeepney/io/blocking.py": 4699555965372200059, + "venv/lib/python3.13/site-packages/pygments/styles/abap.py": 11543320440974964547, + "venv/lib/python3.13/site-packages/pygments/lexers/_cl_builtins.py": 16540712925368297957, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pytest/py.typed": 15130871412783076140, + "backend/src/api/routes/translate/_schedule_routes.py": 18139325016758974063, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/registry.py": 13306388361671753373, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_period.py": 6255181868181182780, + "backend/src/plugins/translate/__tests__/test_dictionary_filter.py": 2426844436521732941, + "docs/api/html/group__test__submit__correction__creates__entry.html": 16912801094355529454, + "backend/git_repos/remote/test-repo.git/hooks/pre-push.sample": 8029776382195911768, + "docs/api/html/group__test__baseline__expired__full__mode.html": 2044862420922151303, + "venv/lib/python3.13/site-packages/pandas/core/window/doc.py": 4204367435727844696, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.cpython-313-x86_64-linux-gnu.so": 14468234464367691795, + "docs/api/html/group__get__dashboard__detail__async.html": 13075524156311537422, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/requests.py": 1102963893193914979, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/base.py": 10205051944814316388, + "venv/lib/python3.13/site-packages/PIL/ImageDraw2.py": 13610162107593278891, + "frontend/src/routes/migration/mappings/+page.svelte": 16228868610649831469, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/apps.py": 4633269405597895766, + "docs/api/html/group__test__commit__changes__wraps__unexpected__error__as__500.html": 2137933037595901582, + "docs/api/html/group__test__get__database__mappings__env__not__found.html": 5367128710946773652, + "venv/lib/python3.13/site-packages/pandas/tests/test_optional_dependency.py": 7930709646653025355, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraypad.pyi": 12178766981280242620, + "backend/tests/services/clean_release/test_compliance_task_integration.py": 7028304091055401940, + "docs/api/html/group__UserUpdate.html": 17001565136687815956, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/RECORD": 11904769847385571600, + "frontend/build/_app/immutable/nodes/16.DMuzgn95.js": 1884713457663303686, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_insert.py": 17383986803728810587, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/util.py": 10640907133050862176, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssltransport.py": 8148652555908329980, + "docs/api/html/group__EnvironmentsApi.html": 15457246866592390632, + "docs/api/html/group__test__get__dashboard__detail__env__not__found.html": 9982209556180665563, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598Warn.f90": 7589258200398878529, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/replace.py": 1708865864686705833, + "docs/api/html/group__TestAPIKeyModel.html": 8065083023674038153, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/__init__.py": 15130871412783076140, + "frontend/src/lib/ui/HelpTooltip.svelte": 15082693035830243831, + "venv/lib/python3.13/site-packages/pandas/core/computation/expressions.py": 10684284531040203747, + "frontend/src/routes/settings/git/+page.svelte": 13757324397807811651, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh22648.pyf": 15094859289098837494, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccosh.csv": 5882025767947638305, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename.py": 16148017223289654683, + "docs/api/html/group__DebugToolPage.html": 15312548458590735605, + "venv/lib/python3.13/site-packages/passlib/__init__.py": 6987142392711441525, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/const_vs_enum.py": 12873459583943748931, + "venv/lib/python3.13/site-packages/pandas/io/pickle.py": 5418646169167774144, + "docs/api/html/group__TranslationEventLog.html": 8646269919159445552, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py": 1041624619197012753, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.pyx": 5760108632332714287, + "docs/api/html/group__update__role.html": 15657294037902023797, + "venv/bin/pyrsa-encrypt": 5858278204727417482, + "frontend/src/components/TaskHistory.svelte": 4575283937108155085, + "venv/lib/python3.13/site-packages/ecdsa/_version.py": 17707102226558781833, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py": 11185011314498348980, + "backend/src/services/reports/__tests__/test_report_normalizer.py": 964299027272685960, + "docs/api/html/globals_func_j.html": 9168488517116821616, + "docs/api/html/search/functions_10.js": 7871242740222700745, + "venv/lib/python3.13/site-packages/authlib/oauth2/auth.py": 8455258275234626534, + "docs/api/html/group__test__auth__db__dev__fallback__works.html": 4992951578259739834, + "frontend/src/components/tasks/LogEntryRow.svelte": 7224781044020990429, + "docs/api/html/group__record__clarification__feedback.html": 15926160429308910947, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_shift.py": 14681514853406253657, + "docs/api/html/group____task__matches__dashboard.html": 12761291922272569962, + "venv/lib/python3.13/site-packages/pygments/lexers/fortran.py": 6859638690245583968, + "venv/lib/python3.13/site-packages/pygments/lexers/textedit.py": 14872291021083255414, + "docs/api/html/group__TranslateCorrectionTests.html": 1332351481274602438, + "docs/api/html/group__TaskManager.html": 14062251301224084851, + "docs/api/html/group__test__create__task__success.html": 17553466713847616318, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.py": 4265012785873472600, + "venv/lib/python3.13/site-packages/pandas/io/excel/_util.py": 6664989864423807444, + "venv/lib/python3.13/site-packages/anyio/_core/_fileio.py": 4507100568401692983, + "venv/lib/python3.13/site-packages/PIL/GifImagePlugin.py": 16073125611077200693, + "docs/api/html/group__test__prepare__candidate__api__success.html": 2373678927354742506, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_sorting.py": 8944017570338511677, + "backend/src/schemas/__init__.py": 8700952869915187087, + "backend/src/models/llm.py": 13905049309473361403, + "docs/api/html/group__get__task__log__stats.html": 13399644287740867608, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/RECORD": 11111103193315065053, + "docs/api/html/group__test__delete__tasks.html": 5667196877695653798, + "docs/api/html/group__DashboardHub__getValidationBadgeClass__Function.html": 6720343436632169859, + "docs/api/html/group__LLMAnalysisPlugin.html": 4902416040278143520, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/apps.py": 14151733532064278336, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlsxwriter.py": 8445547874832015203, + "docs/api/html/group__test__get__task__not__found.html": 13878984338730956196, + "docs/api/html/tab_sd.png": 7906432400932174880, + "docs/api/html/group__ValidationHistory.html": 75479766067234130, + "docs/api/html/group__isItemAllowed__Function.html": 1849064607972812978, + "scripts/check_llm_certs.py": 369377331253345057, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__test__artifact__import__contract.html": 17628340742855449745, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/METADATA": 10707390829735099042, + "venv/lib/python3.13/site-packages/pygments/filter.py": 16846063190466267618, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_formats.py": 17821648708413143557, + "frontend/build/_app/immutable/chunks/BmgRnysp.js": 3131401948912855986, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_unary.py": 2573825678342936734, + "docs/api/html/group__test__code__to__lang__mapping.html": 11085633812653398836, + "venv/lib/python3.13/site-packages/cffi/_cffi_include.h": 13094899814611950155, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/hybrid.py": 5793056975404534989, + "docs/api/html/group__test__get__dashboards__empty.html": 10251761497684394331, + "docs/api/html/group__PolicyEngine.html": 10697573766276435812, + "venv/lib/python3.13/site-packages/pygments/lexers/floscript.py": 944042220740663491, + "venv/lib/python3.13/site-packages/gitdb/exc.py": 13975852664048821278, + "backend/src/services/__tests__/test_llm_plugin_persistence.py": 17921598588767566887, + "docs/api/html/group__parse__position__json.html": 14006761960498316468, + "docs/api/html/group__SupersetClientGetDatasetDetail.html": 13570997899949707169, + "docs/api/html/search/all_2.js": 12325006021233833726, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/hooks.py": 9888227370911151341, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__extract__native__filters__from__permalink__async.html": 7707192445251368687, + "docs/api/html/group__DatasetReviewSessionVersionConflictError.html": 6831719684562983772, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/x509.pyi": 17458894097215345719, + "docs/api/html/group__DashboardHub__getFilterOptions__Function.html": 17463321849622099527, + "docs/api/html/group__InvalidZipFormatError.html": 13344492549849220399, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/dsa.pyi": 1065808244870331598, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/fromnumeric.py": 3792079757208879362, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/console.py": 3540660073442483692, + "docs/api/html/group__AssistantFirstMessageIntegrationTest__Module.html": 16939727239361198220, + "venv/lib/python3.13/site-packages/attrs/filters.py": 6844006230924162452, + "venv/lib/python3.13/site-packages/pydantic/__init__.py": 11536052293866174672, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix_py.py": 6653895521961110080, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp.cpython-313-x86_64-linux-gnu.so": 14187094676876471848, + "venv/lib/python3.13/site-packages/apscheduler/executors/__init__.py": 15130871412783076140, + "frontend/src/lib/stores/translationRun.js": 10321032619900021645, + "venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py": 8657533190026387896, + "docs/api/html/group__test__clean__release__cli.html": 3665197736868730317, + "venv/lib/python3.13/site-packages/fastapi/security/utils.py": 2616633862993539558, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/WHEEL": 16097436423493754389, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.py": 12636954896699095212, + "docs/api/html/group____FakeConfigManager.html": 11213537801456913525, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_constructors.py": 7466113171256856591, + "venv/lib/python3.13/site-packages/numpy/linalg/__init__.py": 15647756307608142577, + "docs/api/html/group____get__linked__dashboards.html": 4945121824645468559, + "frontend/src/routes/settings/settings-utils.js": 1940619437304867415, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/cymysql.py": 5570881524647789741, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____listEvents.html": 2536379713149954797, + "docs/api/html/group__apply__retention__policy.html": 3669478677806189613, + "docs/api/html/group__TranslationRunStore.html": 630348523706519639, + "docs/api/html/group__get__latest__preview.html": 13518123627661036753, + "docs/api/html/group__get__config.html": 8145620257276901599, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/interfaces.py": 4890958419958257958, + "docs/api/html/group__test__us3__mapping__patch__approval__preview__and__launch__endpoints.html": 12512918894261325848, + "docs/api/html/group__handleResumeMigration__Function.html": 578618105352867634, + "backend/tests/test_translate_scheduler_guard.py": 16301771861935445812, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/__init__.py": 10470644151952252331, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/json.py": 14755215397454910426, + "docs/api/html/group__TaskLogger.html": 15092573139764984795, + "venv/lib/python3.13/site-packages/keyring/testing/util.py": 11195868112805409554, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/resource_protector.py": 3563910821279222695, + "docs/api/html/group__ReportsApi__Module.html": 17635169210537766774, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py": 3515239440812977025, + "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.pyi": 6534825471013879522, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_astype.py": 1961831758405941258, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/secrets.py": 17816662030600756342, + "docs/api/html/group__frontend__src__services__storageService__js____downloadFileUrl.html": 12590721399669422477, + "docs/api/html/group__TranslateJobResponse.html": 12927067343734685379, + "venv/lib/python3.13/site-packages/pandas/io/formats/printing.py": 3054136648863371285, + "backend/src/services/__tests__/test_health_service.py": 2102593218438115143, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_api.py": 7131036004750554905, + "docs/api/html/group__job__to__response.html": 14491060891738225447, + "docs/api/html/group__SupersetClientGetDatasetsSummary.html": 13566132585635326488, + "docs/api/html/group____get__clarification__engine.html": 14952466145960727021, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_engines.py": 2790877023773269276, + "docs/api/html/group__DatasetList.html": 15827060935687053250, + "docs/api/html/group__datasetReviewSession__Store.html": 2454065544502690848, + "venv/lib/python3.13/site-packages/certifi/__main__.py": 13417658012431779061, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_indexing.py": 546024765756472196, + "venv/lib/python3.13/site-packages/_cffi_backend.cpython-313-x86_64-linux-gnu.so": 829323374625147265, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_stringdtype.py": 8381476847196129074, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_timestamp_method.py": 11393911418463808195, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_autocorr.py": 1092316868398391792, + "frontend/build/_app/immutable/chunks/CwRsNdLw.js": 12572275116226516551, + "venv/lib/python3.13/site-packages/h11/_util.py": 9362339074207904329, + "docs/api/html/group__test__backup__category__unchanged.html": 13517270357463076954, + "venv/lib/python3.13/site-packages/requests/structures.py": 2668010839316715865, + "docs/api/html/group__clean__release__exceptions.html": 16008981423566577546, + "docs/api/html/search/all_0.js": 14023012673265007156, + "venv/lib/python3.13/site-packages/numpy/random/_generator.pyi": 17167649524261942483, + "docs/api/html/group__ClarificationAnswerRequest.html": 11605609458195798499, + "backend/src/api/routes/storage.py": 3921659652525719249, + "frontend/src/components/llm/__tests__/provider_config.integration.test.js": 2807487384272452401, + "docs/api/html/globals_f.html": 11451612662943857082, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlrd.py": 9982457234958102003, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/off.py": 16254524872582859561, + "venv/lib/python3.13/site-packages/dateutil/easter.py": 14631233890122048084, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_astype.py": 7497268667246913350, + "docs/api/html/group__frontend__src__lib__api__validation__js____fetchRunDetail.html": 7905597516894841314, + "venv/lib/python3.13/site-packages/cryptography/x509/__init__.py": 4066756344049307453, + "venv/lib/python3.13/site-packages/pygments/styles/vs.py": 4188729978573652106, + "venv/lib/python3.13/site-packages/pygments/lexers/minecraft.py": 10133579583270214110, + "backend/src/models/task.py": 13936135007931808732, + "backend/src/core/migration/__init__.py": 13075189211840127611, + "docs/api/html/group__SupersetCompatibilityMatrixTests.html": 12258937098540314692, + "docs/api/html/group__test__empty__events.html": 13036218116544520363, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/licenses/LICENSE": 5563356551734678258, + "venv/lib/python3.13/site-packages/pygments/lexers/prql.py": 1159462562014166646, + "docs/api/html/group__notifications.html": 16884491307342259092, + "docs/api/html/group__resolve__Function.html": 442849213114832901, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/METADATA": 14070889071949105584, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_assign.py": 2647902466315482406, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/base.py": 16951268065271931235, + "venv/lib/python3.13/site-packages/PIL/_typing.py": 17407980360703360848, + "venv/lib/python3.13/site-packages/pandas/tseries/frequencies.py": 2262381949755250516, + "docs/api/html/group__RepoInitRequest.html": 5997274099282176233, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/useless_applicator_schemas.py": 8395157526634674143, + "docs/api/html/group__TranslationRecord.html": 17258624764504208878, + "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/npymath.ini": 1071371630695963467, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hiworld.f90": 6321872653169140461, + "docs/api/html/group__DatasetReviewOrchestrator.html": 7010827725071537587, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/RECORD": 10264342250058307301, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-1.csv": 16078879475693341404, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_bool.py": 433342705586552936, + "docs/api/html/group__StorageSettings.html": 7194021643193109279, + "docs/api/html/group__SupersetContextExtractorBase____extract__chart__id__from__state.html": 14330234105370642214, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/__init__.py": 1437580234680265326, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/__init__.py": 6654484356035994666, + "docs/api/html/group__test__us3__preview__endpoint__returns__failed__preview__without__false__dashboard__not__found__contract__drift.html": 951503115224623642, + "venv/lib/python3.13/site-packages/authlib/oidc/core/userinfo.py": 17201158389976619872, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_repeat.py": 16971624301893342868, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/enumerated.py": 16623643153933348755, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_reduction.py": 1049551473665255648, + "docs/api/html/group__MaintenanceEventListResponse.html": 11340834971136314876, + "venv/lib/python3.13/site-packages/PIL/XpmImagePlugin.py": 6644626557482464312, + "docs/api/html/group__FastAPI__App.html": 16248117540359228223, + "docs/api/html/group__handleCreateGiteaRepo__Function.html": 15118530305807816244, + "venv/include/site/python3.13/greenlet/greenlet.h": 16216120538647882082, + "docs/api/html/group__fetch__paginated__count.html": 8726813622685250751, + "venv/lib/python3.13/site-packages/pandas/core/computation/engines.py": 1889823242704769952, + "docs/api/html/group__test__component__file__exists__Function.html": 7176164496911079412, + "docs/api/html/group__TaskService.html": 12725768316127349890, + "venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-x86_64-linux-gnu.so": 16990140871649227895, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo90.f90": 15981076495193915107, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_typing.py": 17589387690638097975, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_get.py": 10255752806860493777, + "docs/api/html/group__docker__frontend__entrypoint__select__nginx__config.html": 5783880392055327340, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_take.py": 14969455660160053080, + "frontend/build/_app/immutable/nodes/23.CTPXAENE.js": 13029765430495412328, + "docs/api/html/group__CleanReleaseApi.html": 15834070851858806583, + "docs/api/html/search/groups_17.js": 7938088704885798457, + "docs/api/html/group__SchedulerModule.html": 11350421649937027060, + "venv/lib/python3.13/site-packages/starlette/middleware/httpsredirect.py": 16837082808103815044, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/RECORD": 3205266407680661753, + "venv/lib/python3.13/site-packages/pandas/core/arrays/__init__.py": 10460719978124302175, + "docs/api/html/group__test__create__git__config__persists__config.html": 1075563316969273922, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/status_codes.py": 17005213506461710202, + "docs/api/html/group__SourceIntakePanel.html": 556586784232827301, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/REQUESTED": 15130871412783076140, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeLlmSettings.html": 11184309114880759934, + "venv/lib/python3.13/site-packages/passlib/hash.py": 5238710192614935875, + "docs/api/html/group__frontend__src__services__adminService__js____deleteRole.html": 151266335644835279, + "venv/lib/python3.13/site-packages/dateutil/parser/__init__.py": 11377431154251655738, + "backend/alembic_test.db": 14995253579408279707, + "docs/api/html/group____ensure__llm__validation__results__columns.html": 11439277965253958659, + "docs/api/html/group__hash__api__key.html": 11201468271890098251, + "venv/lib/python3.13/site-packages/pygments/lexers/lilypond.py": 1088755646568511197, + "venv/lib/python3.13/site-packages/keyring/backends/SecretService.py": 17482492072907438453, + "docs/api/html/group____trigger__validation.html": 7745124024594471315, + "docs/api/html/group____terminal__run.html": 17656532889079902778, + "docs/api/html/group____has__any__permission.html": 3803466462586983731, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/__init__.py": 8924316361489544973, + "docs/api/html/group__ApiKeyRevokeResponse.html": 17229380547322413204, + "docs/api/html/group__TaskContext.html": 16944274604472450502, + "docs/api/html/group__test__delete__task__with__runs__flag.html": 3543661419042224408, + "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.py": 7658234162211356265, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.pyi": 13110894861187422750, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_records.py": 12217508428903265797, + "docs/api/html/group__compute__dict__snapshot__hash.html": 15191419945274873380, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_non_unique.py": 15286893322149413712, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/characteristics.py": 2916810597149066609, + "venv/lib/python3.13/site-packages/pygments/lexers/blueprint.py": 6370363226580199085, + "venv/lib/python3.13/site-packages/passlib/tests/test_pwd.py": 10089138951415591061, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_ops.py": 3875181889032080505, + "docs/api/html/group__ReportQuery.html": 17989748081758748935, + "frontend/build/_app/immutable/chunks/BDz1oh-Z.js": 651204198678316462, + "docs/api/html/group__ClarificationStateResult.html": 2917799947820497100, + "docs/api/html/group__test__clean__release__tui__v2.html": 12931166045640948787, + "venv/lib/python3.13/site-packages/pandas/_config/localization.py": 11197430792343731762, + "frontend/src/lib/components/reports/ReportDetailPanel.svelte": 5659980083465412500, + "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/integration.py": 3085827906490477329, + "docs/api/html/group__get__task__manager.html": 17905058662693391528, + "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/fields.py": 8923326131649837922, + "docs/api/html/group__ComplianceCheckRun.html": 5481441428574708297, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_args_and_kwargs.py": 3381939712123764154, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_parameter.py": 15173522343126004527, + "backend/alembic/versions/2a7b8c9d0e1f_add_target_languages_to_translation_jobs.py": 7201017059554918882, + "venv/lib/python3.13/site-packages/pygments/lexers/math.py": 1647133236144311250, + "venv/lib/python3.13/site-packages/pip/_internal/commands/uninstall.py": 11003472265011262515, + "docs/api/html/group__StatsBar.html": 10845858962596045632, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__imports.html": 8979637474882093794, + "docs/api/html/group__test__build__detector.html": 11732087775356139179, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/io.py": 11072022722163954973, + "venv/lib/python3.13/site-packages/pygments/lexers/chapel.py": 7619603166448145621, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_function_base.py": 15241999259666702619, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/extension_types.py": 1856757457657310231, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_localize.py": 4937713043686953098, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/__init__.py": 2791238481813240414, + "venv/lib/python3.13/site-packages/numpy/matlib.py": 112839038090033740, + "docs/api/html/group__TestLogger.html": 17549525508288890457, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/_conditional.py": 14190849361275154667, + "venv/lib/python3.13/site-packages/pandas/core/window/ewm.py": 12143982379243011357, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_alter_axes.py": 11338712836362534969, + "backend/src/api/routes/git/_repo_operations_routes.py": 18436111265839776773, + "frontend/src/routes/datasets/review/useReviewSession.js": 2360677870127730232, + "docs/api/html/group__count__resolved__questions.html": 17204134154082267008, + "docs/api/html/group__SQLGenerator.html": 14160413998657267182, + "docs/api/html/group__TermCorrectionBulkSubmit.html": 8087312419241004448, + "venv/lib/python3.13/site-packages/passlib/tests/backports.py": 17098448557554464521, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/schema.py": 8358664670620586363, + "venv/lib/python3.13/site-packages/uvicorn/loops/uvloop.py": 5385425357504191161, + "docs/api/html/group__find__affected__dashboards.html": 15893909015363041840, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_polynomial.pyi": 9134949577567498894, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/util.py": 9373891872370603585, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/var_.py": 12699521091170347311, + "venv/lib/python3.13/site-packages/pygments/lexers/yang.py": 7409365668425173823, + "docs/api/html/group__isFeatureEnabled__Function.html": 11649420265376793257, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_masked_matrix.py": 1444179242134203344, + "docs/api/html/group__test__get__reports__handles__mixed__naive__and__aware__datetimes.html": 10017276203058438772, + "docs/api/html/group__SupersetDashboardsListMixin.html": 17489314668228618185, + "venv/lib/python3.13/site-packages/fastapi/middleware/httpsredirect.py": 5775982317459922443, + "docs/api/html/group__frontend__src__lib__api__validation__js____deleteRun.html": 1830404720177429055, + "docs/api/html/group__Toast.html": 15408687886829508162, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Indel_py.py": 9112851333217812996, + "venv/lib/python3.13/site-packages/pydantic/deprecated/copy_internals.py": 1245428320513062166, + "venv/lib/python3.13/site-packages/pip/_internal/utils/subprocess.py": 3095012838116670328, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarraytypes.h": 6765088137325865789, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/twodim_base.pyi": 3773329770170248877, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_intervalindex.py": 5053113227747333759, + "docs/api/html/group__CheckStageStatus.html": 18359275546300366477, + "docs/api/html/group__MigrationStatus.html": 182423064358330923, + "docs/api/html/group__SupersetCompilationAdapter____normalize__preview__response.html": 18275343522306944195, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py": 14946223589457772924, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_round.py": 1299086932398824299, + "venv/lib/python3.13/site-packages/pandas/tests/extension/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_function.py": 14142346897145169685, + "venv/lib/python3.13/site-packages/PIL/_imagingmath.cpython-313-x86_64-linux-gnu.so": 77725273398858074, + "docs/api/html/group__delete__logs__for__tasks.html": 8497322347744097527, + "docs/api/html/group__test__list__runs__empty.html": 2217625981885883059, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__GitConfigRoutes.html": 1739917609920142885, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs12.py": 5946672289916692272, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/names.py": 705261158040926128, + "docs/api/html/group__extract__data__rows.html": 16073979208553120843, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/common/pythoncapi-compat/COPYING": 3474104874623144155, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/WHEEL": 15858869568085970864, + "venv/lib/python3.13/site-packages/uvicorn/importer.py": 5449629250971153435, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_indexing.py": 4063662953863959087, + "venv/lib/python3.13/site-packages/numpy/_core/_internal.py": 13056946834347643109, + "venv/lib/python3.13/site-packages/pygments/styles/zenburn.py": 1031869867933871309, + "docs/api/html/group____resolve__insert__backend.html": 4179734399226191849, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_quoting.py": 1147992698412929658, + "docs/api/html/group__test__get__logs__with__search.html": 18243052795631026287, + "venv/lib/python3.13/site-packages/pandas/io/parsers/arrow_parser_wrapper.py": 11745253171984606573, + "venv/lib/python3.13/site-packages/pygments/lexers/varnish.py": 3608370156692616959, + "docs/api/html/group__AsyncAPIClient__authenticate.html": 9150281512735244674, + "venv/lib/python3.13/site-packages/pygments/lexers/ambient.py": 18106929781201758080, + "venv/lib/python3.13/site-packages/fastapi/middleware/gzip.py": 436886407566263171, + "frontend/src/routes/translate/+page.svelte": 15284819686967093613, + "venv/lib/python3.13/site-packages/packaging/pylock.py": 15346942039643310253, + "backend/src/models/auth.py": 1996270984653008675, + "venv/lib/python3.13/site-packages/passlib/handlers/mssql.py": 2800599582708170195, + "venv/lib/python3.13/site-packages/packaging/specifiers.py": 2800449842990194437, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__main__.py": 111505188798650285, + "frontend/src/services/taskService.js": 11368052191249479533, + "docs/api/html/group__LogEntryRow.html": 18132223528335475191, + "venv/lib/python3.13/site-packages/anyio/abc/_testing.py": 16484863782452886547, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/queue.py": 7820997545569991670, + "frontend/src/services/toolsService.js": 1568142461283466080, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/layout.py": 11554376181083053156, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/prompt.py": 9561640601516933380, + "venv/bin/py.test": 2593802493707545818, + "docs/api/html/group__ScriptsPackage.html": 4686762520162845714, + "docs/api/html/group__test__reject__then__publish__is__blocked.html": 6737257066366586136, + "venv/lib/python3.13/site-packages/starlette/formparsers.py": 9019779181100355515, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_cpu.h": 13799366908726531089, + "frontend/src/lib/auth/__tests__/permissions.test.js": 14871965971459735540, + "docs/api/html/group__frontend__src__services__git__utils__js____resolveDefaultConfig.html": 13108374365819353481, + "docs/api/html/group__create__markdown__chart.html": 4622906128741204576, + "docs/api/html/globals_func_c.html": 11616926192994037587, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/AB.inc": 10441778083831997423, + "docs/api/html/group__test__map__columns__missing__database__id.html": 13354166006915808905, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.pyi": 15114028503984522176, + "venv/lib/python3.13/site-packages/jose/jwt.py": 3405491994268385178, + "backend/src/api/routes/dataset_review_pkg/_dependencies.py": 13344499297407278282, + "docs/api/html/group__fetchTask__Function.html": 3934212245276248677, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/asymmetric_key.py": 14948661854506285135, + "docs/api/html/group__SupersetClientResolveExportFilename.html": 4965049196551993810, + "docs/api/html/group__test__prior__run__no__records__returns__all.html": 18180477270004399232, + "venv/lib/python3.13/site-packages/pyasn1/type/namedval.py": 8273171503516887681, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_categorical.py": 9923236395692943732, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/licenses/LICENSE": 6653202620765194772, + "docs/api/html/group__test__manifest__service__existing__manifest__cannot__be__mutated.html": 856424001917918075, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_array_utils.py": 14682148205379122638, + "docs/api/html/group__GenerateDocsRequest.html": 10436232842092071054, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_regression.py": 9375525053479844400, + "docs/api/html/group__BranchCreate.html": 4283325077168876664, + "venv/lib/python3.13/site-packages/pydantic/v1/config.py": 12050640083719340958, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_isocalendar.py": 5704842983090753914, + "docs/api/html/group__BatchProcessingService.html": 10275821343943605247, + "docs/api/html/group__TASK__TYPE__PROFILES.html": 4360269911551960850, + "backend/src/core/auth/security.py": 11836436636390315707, + "frontend/src/routes/reports/llm/[taskId]/report_page.contract.test.js": 4485532466755188385, + "docs/api/html/group__info.html": 3684644634418690403, + "docs/api/html/group__CorrectionCellTests.html": 16634413555411031846, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_index_col.py": 17604191644943383567, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_contains.py": 17630310725250553247, + "docs/api/html/group__get__resource__mappings.html": 3377084676438392153, + "venv/lib/python3.13/site-packages/idna/__init__.py": 6489437464172324468, + "venv/lib/python3.13/site-packages/pydantic/mypy.py": 10034028441785588819, + "docs/api/html/group__handleBulkPull__Function.html": 10391716980706820447, + "docs/api/html/group__AssistantMessageRecord.html": 3626244854239183247, + "venv/lib/python3.13/site-packages/pandas/core/ops/array_ops.py": 10061020479033421596, + "docs/api/html/group____config__manager.html": 8330722751801305600, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_encoding.py": 6622474614967398246, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/RECORD": 16740224557818847141, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/putmask.py": 10934101690788549526, + "backend/src/scripts/test_dataset_dashboard_relations.py": 11883199369340933897, + "backend/src/core/superset_client/_dashboards_filters.py": 17625905985676889586, + "backend/src/api/routes/translate/_router.py": 18140665710457768874, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/RECORD": 18294169019244826301, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_value_counts.py": 15633881149834196174, + "venv/lib/python3.13/site-packages/pygments/lexers/go.py": 11118104397271513417, + "venv/lib/python3.13/site-packages/pip/_internal/utils/compatibility_tags.py": 15862649917149158291, + "venv/lib/python3.13/site-packages/ecdsa/_compat.py": 13034064768731879065, + "venv/lib/python3.13/site-packages/jeepney/bindgen.py": 7094585345946943534, + "venv/lib/python3.13/site-packages/requests/help.py": 1885413874621672882, + "docs/api/html/group____make__task.html": 17252776482294162924, + "docs/api/html/group____discover__plugin__execute__permissions__cached.html": 2910829467411233416, + "examples/maintenance-api-python.py": 18279360934407602079, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/jwe.py": 10478268710546063196, + "docs/api/html/group__lookup__superset__accounts.html": 11041872730104611728, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/hook-numpy.pyi": 14233146512272397376, + "venv/lib/python3.13/site-packages/uvicorn/__init__.py": 11517239807926185665, + "docs/api/html/group____async__confirmation__summary.html": 4513234989932474240, + "docs/api/html/group__BulkFindReplaceRequest.html": 8587097893554395288, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/operators.py": 5584982192355398255, + "backend/tests/core/test_migration_engine.py": 13974715113294748054, + "venv/lib/python3.13/site-packages/pydantic/functional_validators.py": 5835339743072048940, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_zips.py": 5184894018177148648, + "venv/lib/python3.13/site-packages/pygments/lexers/mosel.py": 1877592344012288438, + "docs/api/html/group__frontend__src__services__git__utils__js____resolveCurrentEnvironmentId.html": 18338234361034582529, + "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.py": 17192211156810267818, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_index.py": 13517883160584591966, + "docs/api/html/group__cancel__Function.html": 843296188658809904, + "backend/tests/test_layout_utils.py": 7601064815150008920, + "docs/api/html/group__frontend__src__lib__api__validation__js____fetchTask.html": 7928201447544956738, + "venv/lib/python3.13/site-packages/pygments/lexers/wgsl.py": 9718384325049172855, + "docs/api/html/group__list__users.html": 952281442614367040, + "docs/api/html/group__test__reports__detail__openapi__required__keys.html": 16165604713274880229, + "docs/api/html/group__trigger__run.html": 4132187777294958921, + "venv/lib/python3.13/site-packages/pandas/core/missing.py": 13332302791063948992, + "venv/lib/python3.13/site-packages/PIL/FpxImagePlugin.py": 18245056799205041866, + "venv/lib/python3.13/site-packages/uvicorn/loops/__init__.py": 15130871412783076140, + "frontend/e2e/tests/git.e2e.js": 10018194126657448104, + "docs/api/html/search/groups_b.js": 9594259470903340735, + "backend/src/schemas/dataset_review.py": 7490971410706145392, + "venv/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py": 991029278889407558, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_repeat.py": 18394463352314692642, + "venv/lib/python3.13/site-packages/pyasn1/type/useful.py": 10313387611927801339, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_numpy_array_equal.py": 2089426719574307767, + "docs/api/html/group__export__documentation.html": 6283874611178772428, + "venv/lib/python3.13/site-packages/_pytest/threadexception.py": 10464996327276961035, + "venv/lib/python3.13/site-packages/attr/_typing_compat.pyi": 2823472697859409877, + "venv/lib/python3.13/site-packages/pip/_internal/locations/_distutils.py": 13559457590971821771, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategy_options.py": 16342048970907383117, + "frontend/src/lib/components/MaintenanceSettingsPanel.svelte": 8672462648876875400, + "venv/lib/python3.13/site-packages/pandas/tests/util/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_operators.py": 12686786829906210592, + "venv/lib/python3.13/site-packages/greenlet/tests/test_gc.py": 6383305344377882524, + "venv/lib/python3.13/site-packages/pydantic/validate_call_decorator.py": 11130572937843915026, + "venv/lib/python3.13/site-packages/_pytest/unraisableexception.py": 4597178803320514675, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/formatters/__init__.py": 8009871891954370829, + "venv/lib/python3.13/site-packages/charset_normalizer/cd.py": 8687667152597941021, + "backend/src/plugins/translate/orchestrator_exec.py": 965301343860940811, + "docs/api/html/group__remove__future.html": 11104884545239660137, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_indexing.py": 5063545640498065297, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/multiarray.pyi": 17232959648651278910, + "frontend/build/_app/immutable/nodes/31.DO8hucoS.js": 17218402217976611960, + "backend/git_repos/remote/test-repo.git/objects/71/47ad0836409b35c1650dddf5fa6329ba61a824": 16601919569600972016, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ec.pyi": 9835891299056543360, + "venv/lib/python3.13/site-packages/pydantic/_internal/_discriminated_union.py": 1818179910730401509, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_app.py": 4329794049222609894, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py": 16998683210177680416, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/python3.npy": 4957143805477405905, + "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/METADATA": 10810011740984652381, + "backend/src/plugins/translate/dictionary_filter.py": 18047895364995512274, + "venv/lib/python3.13/site-packages/secretstorage/py.typed": 15130871412783076140, + "docs/api/html/group__ValidationSchemas.html": 15986503582536227156, + "venv/lib/python3.13/site-packages/pydantic/v1/__init__.py": 1642038155370734658, + "docs/api/html/group__test__permission__denied__non__admin.html": 13139728334206364468, + "frontend/src/lib/components/translate/TranslationRunResult.svelte": 4434171203266080686, + "docs/api/html/group__DictionaryBatchFilter.html": 14716813921994661678, + "docs/api/html/group__seed__superset__load__data.html": 314813090128633817, + "docs/api/html/group__get__chart__async.html": 1917535031433692607, + "docs/api/html/navtreeindex2.js": 14264443857454783261, + "docs/api/html/group__test__resolve__trusted__policy__snapshots__missing__registry.html": 7913805907359428046, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/interface.py": 3134071530627935964, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/sync.py": 6189260178302531376, + "backend/git_repos/remote/test-repo.git/hooks/pre-applypatch.sample": 15598058327720909861, + "docs/api/html/group__DatasetsApi.html": 7357837522405925673, + "venv/lib/python3.13/site-packages/pygments/lexers/bibtex.py": 14652372822443154388, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/state.py": 10500170103357297551, + "docs/api/html/group__fetchTasks__Function.html": 2358966877665297910, + "venv/lib/python3.13/site-packages/httpx/_transports/mock.py": 13564349939188292363, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/__init__.py": 10261654903106416888, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby.py": 16180421154734310873, + "venv/lib/python3.13/site-packages/pip/_internal/commands/cache.py": 1892313355296776523, + "venv/lib/python3.13/site-packages/PIL/PcfFontFile.py": 8867692961463798279, + "docs/api/html/group____write__dashboard__yaml.html": 9223157474938909024, + "docs/api/html/group____get__or__init__publications__store.html": 8202901534868859807, + "docs/api/html/group__SupersetContextExtractorBase__build__recovery__summary.html": 4966078538740573780, + "docs/api/html/group__frontend__src__services__adminService__js____createUser.html": 12401425458929275331, + "venv/lib/python3.13/site-packages/pandas/io/parquet.py": 14415622950469421518, + "venv/lib/python3.13/site-packages/yaml/constructor.py": 8731384179016992045, + "docs/api/html/group__TaskStatus.html": 16939779299568979357, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/WHEEL": 9788895026336324100, + "docs/api/html/group____build__validation__export.html": 10674121092394091734, + "docs/api/html/group____transform__yaml.html": 11756968780316598035, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_array_ops.py": 6709502157317299163, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_analytics.py": 8155550493311083506, + "venv/lib/python3.13/site-packages/pygments/lexers/_sourcemod_builtins.py": 11809503654078535014, + "docs/api/html/group__ManifestConsistencyStage.html": 12225398414320906138, + "docs/api/html/group__test__mixed__jinja__and__sql.html": 6942646961625701929, + "venv/lib/python3.13/site-packages/numpy/random/_philox.pyi": 7503018432880404291, + "docs/api/html/group__NavigationMock__Module.html": 931761641099207828, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_py.py": 6518425342376455821, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_http_headers.py": 13187746870209705951, + "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py": 7304392396276280230, + "venv/lib/python3.13/site-packages/pygments/lexers/objective.py": 10966124906622044512, + "docs/api/html/folderclosed.svg": 9648391834591542818, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/__init__.py": 2436195108273216959, + "backend/src/services/clean_release/exceptions.py": 16765031947674227117, + "docs/api/html/group__get__dashboard__thumbnail.html": 4640268631621939949, + "backend/src/api/routes/git/__init__.py": 12363460647192569481, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.h": 4516275133897904047, + "docs/api/html/group__LLMProviderType.html": 16741126194237819812, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_day.py": 6371851881186970754, + "docs/api/html/group__mock__api__deps.html": 6995100017269636667, + "venv/lib/python3.13/site-packages/bcrypt/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/__init__.py": 15130871412783076140, + "docs/api/html/group__ArtifactFormat.html": 11975137496264852869, + "venv/lib/python3.13/site-packages/pandas/util/__init__.py": 4070574585004336241, + "docs/api/html/group__backup__dashboards.html": 13922001482280219867, + "docs/api/html/group__listFiles__Function.html": 10223277943043127993, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_range.py": 3544406211185340973, + "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/licenses/LICENSE": 8364608327598414179, + "venv/lib/python3.13/site-packages/yaml/tokens.py": 2279161238859232338, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libselinux-0922c95c.so.1": 16516617710645376769, + "docs/api/html/group__run__publish.html": 18279744766750814950, + "docs/api/html/group____compute__key__hash.html": 364627491871046759, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/__init__.py": 5118993772178969104, + "frontend/postcss.config.js": 5714274776976071678, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/__init__.py": 71159121705871510, + "backend/src/services/notifications/__init__.py": 16179868089470006686, + "docs/api/html/group__valid__job__payload.html": 7175119685712825438, + "venv/lib/python3.13/site-packages/anyio/_core/_subprocesses.py": 7271614043537119143, + "backend/git_repos/remote/test-repo.git/objects/ae/8d2f5dec5d421b2d6cc1eebaad6d2ef901a90d": 15877648001251681939, + "docs/api/html/group__DATABASE__URL.html": 863413084908346717, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_object_arr.py": 4725156416161661681, + "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/RECORD": 9678861487447993169, + "docs/api/html/group____load__confirmation__from__db.html": 8275222931299853306, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_comment.py": 5603075924832933306, + "backend/src/services/maintenance/_orchestrators.py": 15296131977659298194, + "docs/api/html/group__DashboardHub__selectAllColumnFilterValues__Function.html": 12068777279564411579, + "docs/api/html/group____require__clarification__flag.html": 7381291501052368444, + "venv/lib/python3.13/site-packages/pandas/_libs/ops_dispatch.pyi": 16176428602589420913, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getPrimaryActionCtaLabel.html": 14241540356676774140, + "docs/api/html/group__SqlTableExtractorModule.html": 7193260644238587422, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/indexable.py": 4131796922597075758, + "venv/lib/python3.13/site-packages/iniconfig/__init__.py": 12623265899068631092, + "backend/src/api/routes/maintenance/_router.py": 1368406181799456173, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_py.py": 6428078990080243608, + "docs/api/html/group__GitServiceGithubMixin.html": 6075588890758025132, + "venv/lib/python3.13/site-packages/pygments/cmdline.py": 15628499088643246809, + "frontend/build/_app/immutable/chunks/Dj5mA_rO.js": 12791494732355054502, + "venv/lib/python3.13/site-packages/pygments/lexers/php.py": 17636450950688953288, + "venv/lib/python3.13/site-packages/pygments/lexers/arturo.py": 4565524340706235725, + "venv/lib/python3.13/site-packages/websockets/protocol.py": 6401227903827084901, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_round.py": 14996007456708990295, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pygments/lexers/maxima.py": 11824576541091274641, + "venv/lib/python3.13/site-packages/pygments/lexers/procfile.py": 3860651022061090738, + "venv/lib/python3.13/site-packages/httpx/_transports/base.py": 6498990531443644896, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/named_types.py": 6099185243355915896, + "venv/lib/python3.13/site-packages/pygments/lexers/cplint.py": 13990118342267278047, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_case_when.py": 4133174129660834805, + "docs/api/html/group__test__jinja__string__concat.html": 16396089337050427793, + "docs/api/html/group__ValidationFindingsPanelUxTests__Module.html": 3492500435759183400, + "docs/api/html/group__AppConfigRecord.html": 6504206258479760211, + "venv/lib/python3.13/site-packages/pandas/io/formats/_color_data.py": 15614976586613738953, + "docs/api/html/group__name.html": 9476374294755334273, + "docs/api/html/group__PreviewStatus.html": 7092182377354477188, + "docs/api/html/group__PreviewPromptBuilder.html": 1054343519842154092, + "docs/api/html/group__health__service.html": 14616989535045356318, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.pyi": 9003794318682878317, + "docs/api/html/group__frontend__src__services__adminService__js____updateLoggingConfig.html": 2515261969785467260, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/__init__.py": 5027355874704300832, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pxd": 9126772093305708921, + "backend/src/models/mapping.py": 11914532445614622372, + "venv/lib/python3.13/site-packages/pygments/lexers/erlang.py": 11757064262773184741, + "backend/src/services/git/_url.py": 11387722385592868210, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_select.py": 17823338162428379468, + "docs/api/html/group__test__preview__prefers__supported__client__method__before__network__fallback.html": 4750969353248108687, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_endian.h": 2761502363895204652, + "docs/api/html/group__get__preferences.html": 14948180484738743961, + "docs/api/html/group__MetricItem.html": 9045572594838566942, + "venv/lib/python3.13/site-packages/PIL/PcdImagePlugin.py": 922809545463280224, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_iceberg.py": 1726421917529457420, + "venv/lib/python3.13/site-packages/pandas/io/api.py": 8275376846208437741, + "venv/lib/python3.13/site-packages/numpy/polynomial/polynomial.pyi": 9935587108964614958, + "venv/lib/python3.13/site-packages/pip/__init__.py": 9449893869469432503, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/criterion.py": 2042509334195547606, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_cat.py": 15988884721768567895, + "docs/api/html/group__test__sync__network__404__mapping__translates__dashboard__endpoints.html": 1260303454387268885, + "venv/lib/python3.13/site-packages/pip/_internal/utils/filetypes.py": 4415340223268174913, + "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py": 16488635694638612020, + "venv/lib/python3.13/site-packages/keyring/backends/kwallet.py": 3071915635647280308, + "venv/lib/python3.13/site-packages/websockets/sync/server.py": 7966978001578584901, + "docs/api/html/group__Permissions__Module.html": 12197326930694600842, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/RECORD": 2024211292009381421, + "venv/lib/python3.13/site-packages/pip/_internal/commands/search.py": 8292218231992728142, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/terminal_theme.py": 13727399800263579809, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_truncate.py": 9623613657871046837, + "docs/api/html/group__test__get__dashboard__tasks__history__sorting.html": 2186049035541654238, + "venv/lib/python3.13/site-packages/pandas/core/tools/timedeltas.py": 7221393731712061943, + "backend/src/plugins/git/llm_extension.py": 5713196889309291531, + "venv/lib/python3.13/site-packages/dateutil/parser/_parser.py": 9443336890192151397, + "docs/api/html/group__FilterConfidenceState.html": 10329024197508984898, + "docs/api/html/group__Input.html": 4271267002431901392, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iloc.py": 7586049606891014270, + "docs/api/html/group__ADGroupMapping.html": 9762691734208300582, + "frontend/src/lib/api/translate/jobs.js": 9613844538417291513, + "backend/src/core/__tests__/test_config_manager_compat.py": 16202699597948342431, + "venv/lib/python3.13/site-packages/pygments/formatters/_mapping.py": 13335227913146542481, + "docs/api/html/group__test__task__crash__mid__run__marks__failed.html": 17772358674218724371, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pymssql.py": 6650008663791598065, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_ordered.py": 13827016781743929164, + "backend/src/core/utils/fileio.py": 8848681882907859355, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_misc.py": 8706265665356094801, + "backend/src/api/routes/dashboards/_projection.py": 9178951020532547334, + "docs/api/html/group__test__empty__tables.html": 4751367125471660220, + "docs/api/html/group__test__environment__connection.html": 7656214946341009354, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_value_counts.py": 13270420637302622174, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_astype.py": 8633811117672837999, + "docs/api/html/group__TranslationMetrics.html": 4038252507282181238, + "docs/api/html/group__frontend__src__services__git__utils__js____stageBadgeClass.html": 563611682311771515, + "venv/lib/python3.13/site-packages/pip/_internal/cli/main.py": 9304034369762028739, + "venv/lib/python3.13/site-packages/numpy/_typing/_add_docstring.py": 14657346268890680694, + "frontend/src/lib/components/translate/CorrectionCell.svelte": 17398304999652244378, + "docs/api/html/group__AssistantLlmPlanner.html": 3353552519708970803, + "docs/api/html/group__LoggingConfigResponse.html": 16867400746359802764, + "venv/lib/python3.13/site-packages/pip/_internal/utils/datetime.py": 6816292030061301011, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/__init__.py": 12911374670789466961, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_real.f90": 9097998998863007705, + "docs/api/html/group__test__api__key__auth__invalid.html": 11539496437643571562, + "docs/api/html/group__GiteaRepoCreateRequest.html": 610515655183349355, + "frontend/build/_app/immutable/chunks/BzI_u66a.js": 2863505034646388532, + "docs/api/html/group__TestCleanReleaseLegacyCompat.html": 15482783963436488047, + "backend/git_repos/remote/test-repo.git/hooks/post-update.sample": 4288868784459664203, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.cpython-313-x86_64-linux-gnu.so": 6157288238855118017, + "venv/lib/python3.13/site-packages/PIL/AvifImagePlugin.py": 1872412416505003364, + "docs/api/html/group__RunExecutionService.html": 14883816129062720567, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctanh.csv": 8620268345075425735, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_index.py": 1551768441102117466, + "docs/api/html/search/all_15.js": 13325339458310079443, + "backend/src/core/task_manager/manager.py": 8515441209333198629, + "docs/api/html/group__get__task__logs.html": 5609188290893457831, + "backend/src/scripts/__init__.py": 5400372851656282343, + "venv/lib/python3.13/site-packages/pandas/io/__init__.py": 9767690163638898709, + "docs/api/html/group__TranslationBatchResponse.html": 15097744887458440847, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_asof.py": 8656532774394927388, + "backend/src/plugins/translate/orchestrator_aggregator.py": 1906675302571034503, + "venv/lib/python3.13/site-packages/pygments/lexers/_usd_builtins.py": 8671446144574174964, + "backend/alembic/versions/2df63b7ce038_add_missing_columns_policy_id_provider_.py": 9310740736208742201, + "venv/lib/python3.13/site-packages/pandas/_libs/algos.cpython-313-x86_64-linux-gnu.so": 15706331654215762588, + "run_clean_tui.sh": 14305120550349644601, + "venv/lib/python3.13/site-packages/apscheduler/executors/debug.py": 14072221218785494847, + "venv/lib/python3.13/site-packages/_pytest/assertion/truncate.py": 14000392306994773741, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/licenses/LICENSE": 14524289631770661180, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/licenses/LICENSE": 3868190977070717994, + "venv/lib/python3.13/site-packages/_pytest/assertion/__init__.py": 16392348866405910618, + "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.pyi": 17903213294220335051, + "docs/api/html/group__AssistantAuditRecord.html": 15859560198913853671, + "docs/api/html/group__PermissionsTest__Module.html": 11555877868744698217, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_custom_dtypes.py": 12322100282959605055, + "docs/api/html/group__sanitize__filename.html": 18087990957535401473, + "docs/api/html/group__edit__dictionary__entry.html": 12444753978279908445, + "docs/api/html/group__ViolationCategory.html": 7515224565172006703, + "venv/lib/python3.13/site-packages/numpy/_core/overrides.pyi": 13121243505132110607, + "venv/lib/python3.13/site-packages/numpy/_core/numeric.py": 12261531924304979741, + "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.pyi": 10487441680587530350, + "venv/lib/python3.13/site-packages/sqlalchemy/util/deprecations.py": 7020540470348910770, + "docs/api/html/group____apply__git__identity__from__profile.html": 14258939660260091015, + "docs/api/html/group__test__git__service__repositorys__typo__regression.html": 12032073559759337916, + "docs/api/html/group__TaskCleanupService.html": 1939137499434761984, + "docs/api/html/group__load__persisted__tasks.html": 897838907183043727, + "docs/api/html/group__list__roles.html": 13386931894504978817, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_as_unit.py": 8492383355919959393, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arithmetic.py": 14858832264660410908, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/testing.pyi": 16263777835572515739, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/RECORD": 8104089156015954779, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__frontend__src__services__git__utils__js____tryParseJsonObject.html": 8683269790796874276, + "venv/lib/python3.13/site-packages/pandas/core/_numba/extensions.py": 4132402557064663751, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex_like.py": 1407890639206728014, + "venv/lib/python3.13/site-packages/pandas/core/indexes/multi.py": 9115751979327393200, + "docs/api/html/group__resolve__bound__provider__id.html": 6473196790874356733, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/__init__.py": 12906995259233516197, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_duplicated.py": 6659959750254523427, + "backend/git_repos/remote/test-repo.git/objects/59/3a37acec7d2bddce10df90ab17bc9ede52e523": 1117161574576783985, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py": 10093932131797570986, + "backend/_batch_convert_defs.py": 1082521689509525414, + "docs/api/html/group__SidebarNavigation__Module.html": 15358237226164093988, + "docs/api/html/group__test__report__models.html": 3181628753533046372, + "docs/api/html/group__test__tui__f8__blocked__without__facade__binding.html": 5329039055415769403, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/RECORD": 8956798822782402202, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/oct_key.py": 11504245602045724, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/palette.py": 17383863392192572184, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby_subclass.py": 5540948578269213810, + "docs/api/html/group______init____.html": 12200029640079662818, + "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/__init__.py": 1229691061011958769, + "docs/api/html/group__resolveTask__Function.html": 18339026573508690713, + "venv/lib/python3.13/site-packages/packaging/licenses/__init__.py": 15879493360260891293, + "frontend/build/_app/immutable/chunks/BJ5Ljdqb.js": 13817746092787830185, + "docs/api/html/group____save__config__to__db.html": 11046536065710831313, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata.py": 2021612995853910808, + "backend/src/plugins/debug.py": 13683182174969777939, + "venv/lib/python3.13/site-packages/pandas/tseries/holiday.py": 1134358927599488799, + "backend/src/models/__tests__/test_clean_release.py": 9937664704633848570, + "venv/lib/python3.13/site-packages/numpy/fft/tests/test_pocketfft.py": 18025182347224285050, + "venv/lib/python3.13/site-packages/ecdsa/keys.py": 10281170640484982154, + "venv/lib/python3.13/site-packages/pygments/styles/nord.py": 8469538903422182155, + "venv/lib/python3.13/site-packages/jose/jwk.py": 2781217726458801910, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_round_trip.py": 14684057343786032776, + "backend/src/core/auth/oauth.py": 2441694487419087464, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_python_parser_only.py": 12231449578582108346, + "backend/src/api/routes/migration.py": 5331313337326650916, + "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.py": 15031197044862908440, + "docs/api/html/group____get__profile__filter__binding.html": 4542513203441105089, + "backend/src/plugins/translate/preview_token_estimator.py": 254295902082954386, + "venv/lib/python3.13/site-packages/sqlalchemy/event/base.py": 18238453132776154057, + "venv/lib/python3.13/site-packages/pygments/lexers/tnt.py": 4657130609241407766, + "venv/lib/python3.13/site-packages/numpy/exceptions.py": 10981418339663118022, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/wheel.py": 828222664427570058, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/WHEEL": 5076688779572520166, + "venv/lib/python3.13/site-packages/pillow.libs/libsharpyuv-95d8a097.so.0.1.2": 5320647379976541240, + "venv/lib/python3.13/site-packages/fastapi/security/api_key.py": 6965937678104291169, + "docs/api/html/group__SupersetContextParsingMixin__parse__superset__link.html": 5643145243445652253, + "docs/api/html/group__frontend__src__lib__api__validation__js____triggerRun.html": 4533127585714595290, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/npyio.pyi": 14147712281086095231, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_api.py": 14120915875574640262, + "docs/api/html/group__record__clarification__answer.html": 18379043236673928126, + "frontend/src/components/git/useGitManager.js": 17753445373937744503, + "docs/api/html/group__ProfileSecuritySummary.html": 13465338754935140981, + "docs/api/html/search/functions_1.js": 11481182983342275162, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____getTaskForResource.html": 14513402972391639964, + "docs/api/html/group__PreviewRow.html": 89719087155129857, + "venv/lib/python3.13/site-packages/gitdb/db/base.py": 3375449584942578385, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_old_ma.py": 18116460138221458026, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarmath.py": 9762951701352556447, + "docs/api/html/group__LastTask.html": 9510396811518924648, + "frontend/src/types/backup.ts": 1741234034896689348, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fft.pyi": 14454280827038327842, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.py": 15511055314565873928, + "docs/api/html/group__PrepareCandidateRequest.html": 3237622670833360342, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nditer.py": 8341339612899904236, + "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.py": 2234931733687557433, + "venv/lib/python3.13/site-packages/numpy/doc/ufuncs.py": 11706048944901534664, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_datetimelike.py": 15246580603900811749, + "docs/api/html/group__mask__secret__value.html": 2433163108890976342, + "docs/api/html/group__approve__candidate__endpoint.html": 16706174447280477431, + "docs/api/html/group__test__metric__snapshot__stores__per__language.html": 12291939914587003915, + "venv/lib/python3.13/site-packages/pandas/tests/extension/list/array.py": 8065856342131357421, + "frontend/src/lib/ui/PageHeader.svelte": 16740944781208407079, + "backend/test_pat_api.py": 4206656943734821967, + "venv/lib/python3.13/site-packages/attr/setters.py": 12467854389745942025, + "docs/api/html/group__updateMapping__Function.html": 16524716365093515524, + "venv/lib/python3.13/site-packages/pygments/lexers/agile.py": 5686162551416231457, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/__init__.py": 15130871412783076140, + "docs/api/html/group__APIKey.html": 15496875141447468476, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/api.py": 13134394673408254097, + "backend/src/services/dataset_review/clarification_engine.py": 5693631572548121207, + "docs/api/html/group__loadLoggingConfig__Function.html": 747812477749540392, + "docs/api/html/group__test__mixed__active__and__completed.html": 13263869066783777040, + "docs/api/html/search/all_7.js": 449094548201224648, + "docs/api/html/group__TraceContextMiddlewareModule.html": 16492855860524137184, + "venv/lib/python3.13/site-packages/pygments/lexers/ldap.py": 17455983150181177257, + "docs/api/html/group__JSON__OVERHEAD__PER__ROW.html": 5671854497093663857, + "venv/lib/python3.13/site-packages/pygments/lexers/_csound_builtins.py": 14530739818491398298, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/base_key.py": 10402800587070352851, + "venv/lib/python3.13/site-packages/websockets/asyncio/messages.py": 6848925149930551457, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py": 8530403239168529668, + "docs/api/html/group__QuestionState.html": 7538640298895031118, + "docs/api/html/group__CleanReleaseTuiScript.html": 1449983814226815844, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/METADATA": 6656194161182818439, + "docs/api/html/group__run__check__legacy.html": 14080804246720725799, + "docs/api/html/group__ReportPageContractTest__Module.html": 10375296767582246595, + "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.pyi": 362674842164060822, + "docs/api/html/group__getAssistantHistory__Function.html": 15491234740107286264, + "docs/api/html/navtreeindex22.js": 6671112876915377349, + "venv/lib/python3.13/site-packages/anyio/_core/_tempfile.py": 230288423480899091, + "venv/lib/python3.13/site-packages/pip/_internal/models/target_python.py": 422323125651848065, + "docs/api/html/group____build__preference__response.html": 9896456427595935858, + "docs/api/html/group__test__delete__tasks__empty__list.html": 17672043131209530159, + "docs/api/html/group__get__dataset__ids.html": 11409176167791061487, + "venv/lib/python3.13/site-packages/keyring/compat/py312.py": 17581691618899404491, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/array.py": 1919242530821725197, + "docs/api/html/group__ProfileApiModule.html": 1705516616452689774, + "docs/api/html/group____build__task__observability__summary.html": 8840583376339929930, + "venv/lib/python3.13/site-packages/pandas/core/construction.py": 10646956189518908985, + "docs/api/html/navtreeindex15.js": 13503057042941155951, + "docs/api/html/group__get__scheduler__service.html": 12730756881612997283, + "venv/lib/python3.13/site-packages/numpy/random/c_distributions.pxd": 16574161095720077595, + "docs/api/html/search/functions_13.js": 8491110692914206388, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/auto.py": 15947958974558543657, + "docs/api/html/group__LanguageDetectServiceTests.html": 7434991135670181721, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_set_name.py": 9094608008538024609, + "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.py": 8786558894094307341, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_subclass.py": 12453252499544981915, + "venv/lib/python3.13/site-packages/pygments/lexers/xorg.py": 14591144191879110061, + "docs/api/html/group__update__user.html": 16743247671966182710, + "venv/lib/python3.13/site-packages/PIL/ImageGrab.py": 11114379915762058868, + "backend/tests/test_storage_audit_fixes.py": 311299375809552600, + "docs/api/html/navtreeindex21.js": 7533748426858788437, + "venv/lib/python3.13/site-packages/ecdsa/test_rw_lock.py": 1572802971060634697, + "venv/lib/python3.13/site-packages/numpy/tests/test_warnings.py": 3154249157134620649, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/jws.py": 7505219131537941130, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/conftest.py": 13388202260795998613, + "docs/api/html/group__SupersetClientGetDataset.html": 11557014812456007471, + "venv/lib/python3.13/site-packages/websockets/extensions/__init__.py": 13067201313921664210, + "frontend/src/lib/components/translate/BulkReplaceModal.svelte": 9447617465987205060, + "docs/api/html/group__test__composite__keys.html": 3672461709156266376, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_mixins.py": 12351373879926460719, + "venv/lib/python3.13/site-packages/numpy/_typing/__init__.py": 18242238390157333247, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/RECORD": 16558021503346615951, + "venv/lib/python3.13/site-packages/authlib/__init__.py": 8957417237987377675, + "venv/lib/python3.13/site-packages/sqlalchemy/future/engine.py": 4639404437703898767, + "venv/lib/python3.13/site-packages/PIL/IcoImagePlugin.py": 14483964191652228364, + "docs/api/html/group__test__trigger__run__not__found.html": 13928430633871527174, + "docs/api/html/group__TranslateRoutes.html": 12674068421448873561, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/models.py": 11521700876708601977, + "backend/src/core/rate_limiter.py": 8903493129424330064, + "docs/api/html/group__network__error__handler.html": 11593637847095297203, + "docs/api/html/group____is__masked__or__invalid__api__key.html": 15965724501625265254, + "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/asn1.py": 8801155055979135092, + "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py": 15537957042757255513, + "docs/api/html/group__AsyncAPIClient____is__dashboard__endpoint.html": 11542093610496385064, + "venv/lib/python3.13/site-packages/pandas/core/groupby/indexing.py": 18009437216172136227, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/strings.pyi": 15361498560917514616, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_repr.py": 16609300578827398344, + "docs/api/html/group__progress.html": 3466818030353762768, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_api.py": 6439927798101672423, + "docs/api/html/group__commit__changes.html": 12893499093937665878, + "backend/src/services/reports/__tests__/test_type_profiles.py": 8045459741015920365, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_aix.h": 8706347224105630468, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dh.py": 5728866271812501558, + "venv/lib/python3.13/site-packages/pandas/tests/frame/constructors/test_from_dict.py": 4937173601709369119, + "venv/lib/python3.13/site-packages/PIL/BufrStubImagePlugin.py": 5520941821175895889, + "docs/api/html/tab_h.png": 1719686501894845356, + "docs/api/html/group____default__config.html": 6364950080332281281, + "frontend/src/services/gitService.js": 17955715259947941983, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.cpython-313-x86_64-linux-gnu.so": 1137453907225619959, + "venv/lib/python3.13/site-packages/pygments/lexers/teal.py": 10004159499175716139, + "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.pyi": 11535882608355652318, + "venv/lib/python3.13/site-packages/PIL/TiffTags.py": 8688883943071546469, + "docs/api/html/group__TaskReport.html": 7520791941174149444, + "frontend/playwright-report/trace/index.CzXZzn5A.css": 10367596602957476705, + "docs/api/html/group____templates__imports.html": 12808569599321695443, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_encs.py": 9703572751543253904, + "venv/lib/python3.13/site-packages/passlib/handlers/mysql.py": 11165762635383041460, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pyodbc.py": 8175468565413744516, + "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/RECORD": 2963644632913976754, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_all_methods.py": 18227706645906325527, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/data.py": 9365024327689738663, + "docs/api/html/group__test__key__hash__unique.html": 5562302720760720176, + "venv/lib/python3.13/site-packages/_pytest/scope.py": 7603155109542903644, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/METADATA": 3448868793744358515, + "venv/lib/python3.13/site-packages/_pytest/assertion/rewrite.py": 2425990712758915831, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string_arrow.py": 10008395772220468318, + "venv/lib/python3.13/site-packages/pygments/styles/paraiso_light.py": 15941591873059208999, + "docs/api/html/group__DatasetReviewModels.html": 11351633006564729227, + "frontend/src/lib/components/DashboardMaintenanceBadge.svelte": 9085495636438661604, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/implicit.py": 7379110378465568776, + "docs/api/html/group__test__ad__group__mapping.html": 3525500635803023761, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/liblber-1c9097e2.so.2.0.200": 2651879839738900594, + "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/WHEEL": 8954358347596196608, + "docs/api/html/group__test__multimodal__model__returns__true.html": 4867921283780529014, + "venv/lib/python3.13/site-packages/numpy/_core/getlimits.py": 7842717073693731093, + "frontend/build/_app/immutable/nodes/11.BtEvIQiI.js": 16346045538958982699, + "frontend/build/_app/immutable/chunks/BvYi3lDf.js": 12844549807393242792, + "docs/api/html/group__ResourceType.html": 6653107601280859808, + "venv/lib/python3.13/site-packages/anyio/pytest_plugin.py": 4165280709813718099, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/__init__.py": 635200170992032966, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_value_attrspec.py": 7741590618654448240, + "backend/src/api/routes/admin.py": 14496518311272057274, + "docs/api/html/group__create__user.html": 3710853753512075388, + "docs/api/html/group__AdminApiKeyRoutes.html": 1930874666734065642, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/array.py": 12658302948819867972, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_pydatetime.py": 1129954411080617001, + "backend/src/plugins/translate/_batch_sizer.py": 8388348461987957727, + "venv/lib/python3.13/site-packages/numpy/_core/_dtype_ctypes.py": 5564496700647900501, + "docs/api/html/group__LanguageSwitcher.html": 18011055558279343478, + "docs/api/html/group__test__cot__json__formatter__plain__message.html": 6324270239309032244, + "venv/lib/python3.13/site-packages/_pytest/mark/structures.py": 12237146603608917650, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/exclusions.py": 15852672500377116297, + "docs/api/html/group__handleRunDebug__Function.html": 17645482735921964594, + "venv/lib/python3.13/site-packages/pandas/tests/strings/__init__.py": 5060190599837877599, + "docs/api/html/group__sanitize__username.html": 7200979889774485267, + "docs/api/html/group__test__list__runs__filter__status.html": 7762165836370653178, + "docs/api/html/group__render__prompt.html": 12771717951488483090, + "frontend/build/_app/immutable/nodes/3.BNbN8snG.js": 10461527122478209262, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/processors.pyx": 7085380878820747579, + "docs/api/html/group__test__approveRow__Function.html": 17163537917081477745, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_get_numeric_data.py": 11747933921770969706, + "docs/api/html/group____persist__audit.html": 17877024820327467143, + "docs/api/html/group__GitRepoOperationsRoutes.html": 17274671154448200931, + "venv/lib/python3.13/site-packages/pandas/core/ops/invalid.py": 10186546927135277579, + "backend/src/plugins/translate/dictionary_crud.py": 17858252266960667195, + "docs/api/html/group__PlaywrightConfig.html": 300679414561245473, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/einsumfunc.py": 9944042006439149561, + "docs/api/html/group____is__production__env.html": 8724827897681230971, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/dtype.pyi": 465999539608816848, + "docs/api/html/group__LoginE2E.html": 7883569100009245167, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_polynomial.pyi": 4266795948450921502, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_integer.f90": 8308927438994838783, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_constructors.py": 17392287676196409733, + "docs/api/html/group__test__get__log__stats.html": 13273447744955427101, + "frontend/build/_app/immutable/nodes/7.DNPdCEUP.js": 4146616923678115494, + "docs/api/html/group__AdminApi.html": 767628307398103052, + "venv/lib/python3.13/site-packages/httpx/_multipart.py": 2181282475656908730, + "venv/lib/python3.13/site-packages/certifi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/dependency.py": 7300594038182867460, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_sample.py": 4641567828238191927, + "docs/api/html/group__TaskPersistenceModule.html": 669406050270516144, + "venv/lib/python3.13/site-packages/smmap/test/test_tutorial.py": 16309370344201290582, + "docs/api/html/group__JobLifecycle.html": 18086772682144526037, + "venv/lib/python3.13/site-packages/pandas/core/internals/api.py": 14632030170441237968, + "docs/api/html/group__test__get__dashboard__thumbnail__env__not__found.html": 2691256043030470661, + "venv/lib/python3.13/site-packages/pygments/lexers/kusto.py": 16285533608924436308, + "frontend/src/components/git/GitMergeDialog.svelte": 10276431474456171235, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py": 6510864297510700136, + "venv/lib/python3.13/site-packages/pydantic/experimental/missing_sentinel.py": 1637241331832367709, + "venv/lib/python3.13/site-packages/numpy/__init__.cython-30.pxd": 1476688171642957054, + "venv/lib/python3.13/site-packages/pygments/lexers/phix.py": 16775285097768256238, + "venv/lib/python3.13/site-packages/jaraco/classes/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.pyi": 15130871412783076140, + "docs/api/html/group__isDatasetReviewConflictError__Function.html": 9798021926177492027, + "backend/src/core/utils/superset_context_extractor/_pii.py": 15135596230141304567, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_cumulative.py": 8178093067175155877, + "docs/api/html/group__test__provision__adfs__user__existing.html": 3869094630531615689, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_indexing.py": 14717641281147152159, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_printing.py": 8859757563354666575, + "venv/lib/python3.13/site-packages/more_itertools/__init__.pyi": 16479506520351014608, + "venv/lib/python3.13/site-packages/pygments/lexers/webmisc.py": 9530962803076253849, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_pivot_multilevel.py": 9830258607305392103, + "docs/api/html/group__test__get__dashboard__tasks__history__filters__success.html": 6489322718230741980, + "docs/api/html/group__get__llm__status.html": 4661440239824290304, + "docs/api/html/group__startup__event.html": 11110363748286248660, + "venv/lib/python3.13/site-packages/pandas/_libs/interval.cpython-313-x86_64-linux-gnu.so": 8964760352224019544, + "docs/api/html/group__test__default__config__root__path.html": 13958925806070579987, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/ssh.py": 16262079536832961001, + "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.pyi": 9206355685192026844, + "docs/api/html/group__handleAddOrUpdateEnv__Function.html": 3373509982933940852, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_setops.py": 8622608042929141013, + "docs/api/html/group__create__future.html": 6308958188691584151, + "docs/api/html/group__frontend__src__routes____page__ts____load.html": 10563006296048613280, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.py": 5735043585893007971, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/datonly.f90": 10495925225338237742, + "backend/src/core/superset_client/_datasets_preview.py": 15457586484984946621, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/hkdf.py": 8539232219382152238, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/_py_row.py": 918740441973901148, + "venv/lib/python3.13/site-packages/pandas/_libs/arrays.pyi": 12629806207276576877, + "backend/src/api/routes/mappings.py": 13835851258441126670, + "venv/lib/python3.13/site-packages/pygments/console.py": 3540660073442483692, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/fetch.py": 13937019222549208796, + "venv/lib/python3.13/site-packages/websockets/legacy/handshake.py": 376095144657994920, + "backend/src/models/__init__.py": 13280325675752905975, + "venv/lib/python3.13/site-packages/starlette/__init__.py": 3252794766897205870, + "venv/lib/python3.13/site-packages/urllib3/exceptions.py": 9687187566231423953, + "frontend/static/favicon.svg": 6451919037497541980, + "docs/api/html/group__trigger__preview__generation.html": 17109618387126264866, + "docs/api/html/group__AssistantResolvers.html": 2791936012509790266, + "docs/api/html/group__test__end__already__completed__event.html": 5476557657288556633, + "docs/api/html/group__test__headless__fallback.html": 12179006034022319490, + "backend/src/plugins/git_plugin.py": 11782933307912594901, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_pbkdf2.py": 17016810698578473773, + "docs/api/html/group__test__belief__scope__missing__anchor.html": 6431050729212027713, + "venv/lib/python3.13/site-packages/jsonschema/_utils.py": 14237077105384631097, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/type_api.py": 10299750173867399533, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_conversion.py": 6205112277291935767, + "docs/api/html/group__MigrationJob.html": 8882884563951880280, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/conftest.py": 8406022190801970256, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/arrayterator.py": 3076316311545674897, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_duplicated.py": 14676058112867601208, + "docs/api/html/group__test__get__repository__status__returns__no__repo__payload__for__missing__repo.html": 12740974116041500221, + "backend/src/services/maintenance/__init__.py": 3167476255502727608, + "docs/api/html/group__DictionaryManagerModule.html": 13851818928225468760, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_reindex.py": 15794750188594767026, + "backend/src/scripts/create_admin.py": 12963729348700590154, + "docs/api/html/group__MappingWarningLevel.html": 16869836344481921740, + "docs/api/html/group__test__patch__dashboard__metadata__skips__when__no__metadata.html": 9093107496317548408, + "venv/lib/python3.13/site-packages/itsdangerous/timed.py": 12571102606719555007, + "venv/lib/python3.13/site-packages/fastapi/utils.py": 15402403831700548944, + "venv/lib/python3.13/site-packages/pip/_internal/cli/spinners.py": 7664056305019151182, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.pyi": 5973671421648618563, + "docs/api/html/group__MapperToolPage.html": 15826560549177500594, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_transform.py": 6759417099641034692, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_regression.py": 5652583095949741238, + "venv/lib/python3.13/site-packages/apscheduler/executors/base.py": 1980520974511822101, + "docs/api/html/group__WsLogHandlerModule.html": 1965949101963733420, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_setops.py": 9991233160569430204, + "backend/src/services/dataset_review/event_logger.py": 8944052747593678117, + "docs/api/html/group__DatasetReviewClarificationModels.html": 4185914668418818885, + "docs/api/html/group__filterDashboardsWithRepositories__Function.html": 3751486221728345447, + "venv/lib/python3.13/site-packages/starlette/templating.py": 9242638124487797988, + "venv/lib/python3.13/site-packages/numpy/version.pyi": 5075382895829889457, + "docs/api/html/group__ValidationRunService.html": 9956169665748668204, + "venv/lib/python3.13/site-packages/pandas/util/_decorators.py": 14780166101442507665, + "docs/api/html/group__handleDeleteUser__Function.html": 7705917636267502529, + "venv/lib/python3.13/site-packages/requests/compat.py": 5836590172733661320, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_complex.py": 4290059580478881700, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/spinner.py": 5564789578696016201, + "docs/api/html/group__duplicate__job.html": 4139685743055526761, + "docs/api/html/group__SemanticSourceStatus.html": 14169028665990248848, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_shape_manipulation.py": 12598431412396627042, + "docs/api/html/group__load__bootstrap__artifacts.html": 12225841342507628272, + "docs/api/html/group__test__is__multimodal__model__emits__warning.html": 11946987825238879999, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/box.py": 14303526978791498662, + "docs/api/html/group__test__json__load__if__needed__integer.html": 16897843606184491072, + "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so": 2888939866336522308, + "frontend/src/lib/api/translate/datasources.js": 17202165717960970061, + "frontend/vite.config.js": 3769019895915732131, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/__init__.py": 16416360448593978041, + "frontend/src/lib/api/translate/schedules.js": 8616345664085016472, + "frontend/build/_app/immutable/nodes/28.CyDT2CrL.js": 1261283689922134240, + "docs/api/html/group__apply__preview.html": 12571622568156907711, + "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/numpy/lib/__init__.py": 12211229657513986410, + "docs/api/html/group__DashboardHub__normalizeTaskStatus__Function.html": 805796678502738814, + "venv/lib/python3.13/site-packages/anyio/streams/stapled.py": 9733127303038397237, + "venv/lib/python3.13/site-packages/packaging/_manylinux.py": 8268418900652790584, + "docs/api/html/group____serialize__semantic__field.html": 12631098605309415203, + "venv/lib/python3.13/site-packages/attr/_cmp.pyi": 14443711863388606665, + "venv/lib/python3.13/site-packages/rapidfuzz/utils_cpp.cpython-313-x86_64-linux-gnu.so": 16145829319198889672, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_setops.py": 10550603120090683350, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/encoder.py": 120921270972591880, + "backend/src/plugins/translate/executor.py": 700747250181987365, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/core/interchange/from_dataframe.py": 8278998381856028462, + "docs/api/html/files.html": 4939049439273801457, + "backend/src/api/routes/assistant/__init__.py": 6253863937719648990, + "venv/lib/python3.13/site-packages/h11/_abnf.py": 10456996134895429201, + "docs/api/html/group__MockGitService.html": 15745917040920989657, + "docs/api/html/group____serialize__preview.html": 9415726163648130901, + "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/test_jsonschema_specifications.py": 12693210357718751894, + "venv/lib/python3.13/site-packages/pygments/lexers/rnc.py": 9747352274729854620, + "venv/lib/python3.13/site-packages/rapidfuzz/process.py": 3870006735299537016, + "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.pyi": 10077265709692546934, + "docs/api/html/group__test__no__header__returns__none.html": 15501842573100321816, + "docs/api/html/group____validate__superset__connection__fast.html": 1767320346178459487, + "docs/api/html/group__get__dashboard__tasks__history.html": 3745020964787224389, + "venv/lib/python3.13/site-packages/jaraco/classes/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/urllib3/_collections.py": 3440910792774742262, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/util.py": 2176177435997659667, + "docs/api/html/group__SupersetChartsMixin.html": 10694368076693575185, + "docs/api/html/group__parse__dashboard__url__for__filters__async.html": 8302124891449146063, + "docs/api/html/group__test__baseline__expired__fallback.html": 15287350939065441355, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_exceptions.py": 12145393002202744314, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_value_counts.py": 5534713464063604301, + "docs/api/html/globals_m.html": 5857181081783132873, + "docs/api/html/group__loadRecentTasks__Function.html": 8342396704682270870, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraysetops.pyi": 1782641795267171928, + "venv/lib/python3.13/site-packages/sqlalchemy/util/tool_support.py": 4926732415520779988, + "venv/lib/python3.13/site-packages/pandas/plotting/__init__.py": 13230487326686617492, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/exc.py": 3242780937124999391, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numeric.py": 5627177940393829333, + "docs/api/html/group____serialize__run__context.html": 12922073506719689996, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/__init__.py": 15130871412783076140, + "docs/api/html/search/groups_15.js": 2849271939807724756, + "venv/lib/python3.13/site-packages/fastapi/middleware/wsgi.py": 3706578020491442104, + "docs/api/html/group__run__artifact__import.html": 14280186280620385233, + "backend/git_repos/remote/test-repo.git/description": 15262637409357137373, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_spec_conformance.py": 16448324304809345527, + "docs/api/html/group__getSortStatusValue__Function.html": 16892527846024245299, + "venv/lib/python3.13/site-packages/yaml/events.py": 745772917931702910, + "docs/api/html/group__test__enterprise__policy__missing__prohibited.html": 9934851722239315434, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/interfaces.py": 5371646629319169153, + "docs/api/html/group__NotificationChannel.html": 13421940193975834527, + "docs/api/html/navtreeindex16.js": 883027576628231514, + "docs/api/html/group____sync__raw__payload__from__config.html": 1731463979685810569, + "docs/api/html/group__EnvironmentResponse.html": 9633575500379521671, + "venv/lib/python3.13/site-packages/click/types.py": 6491651131669287534, + "docs/api/html/group__test__git__plugin__fallback__creates__new.html": 3612866020087373120, + "docs/api/html/group__delete__task__with__logs.html": 14316927894050853121, + "docs/api/html/group__test__start__end__time__before__start.html": 18093529765348516001, + "venv/lib/python3.13/site-packages/pandas/tests/test_expressions.py": 16378925536309828288, + "docs/api/html/group__ClarificationStateResponse.html": 8085920956673403769, + "docs/api/html/group__OverrideLanguageRequest.html": 1786154171429944136, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_categorical.py": 10863578845754547294, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/_orm_constructors.py": 4276726508722638048, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/authorization_server.py": 6858244712625056421, + "docs/api/html/group__createRole__Function.html": 17635275329141621796, + "docs/api/html/group__test__get__dashboard__tasks__history__success.html": 3755590925245868067, + "docs/api/html/group__CorrectionCell.html": 13749448967384828012, + "docs/api/html/search/all_17.js": 369213695750280983, + "docs/api/html/group__test__transform__zip__invalid__path.html": 6265988318849736978, + "docs/api/html/group__SupersetClientDoImport.html": 8043177973453792978, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/WHEEL": 9788895026336324100, + "venv/lib/python3.13/site-packages/pandas/tseries/offsets.py": 13750652921710295115, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/__init__.py": 15130871412783076140, + "docs/api/html/group____require__session__version__header.html": 2001910810017263415, + "venv/lib/python3.13/site-packages/pandas/tests/internals/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/METADATA": 1266134223155266488, + "venv/lib/python3.13/site-packages/numpy/exceptions.pyi": 13942486291715303235, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_time_grouper.py": 3203729024111501071, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/RECORD": 6488682447051657004, + "docs/api/html/group__SupersetClientModule.html": 204765713104060593, + "venv/lib/python3.13/site-packages/PIL/_imagingtk.pyi": 18222325750818585549, + "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5.html": 13431474567808681090, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/LICENSE": 8177430849046795595, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_loc.py": 17839380711970027363, + "backend/src/core/encryption_key.py": 11618631956132703846, + "docs/api/html/group__MigrationEngineModule.html": 4559630898828635226, + "docs/api/html/group____get__record.html": 13908384265392820163, + "docs/api/html/group__SessionStatus.html": 3808958363584408274, + "venv/lib/python3.13/site-packages/pandas/core/computation/eval.py": 3176226283265954028, + "docs/api/html/group__AuthConfigModule.html": 14846201783459708048, + "docs/api/html/group__create__admin.html": 11166797148814690447, + "venv/lib/python3.13/site-packages/yaml/error.py": 5626374410579058436, + "docs/api/html/group__test__validate__path__already__exists.html": 13732390823029512954, + "docs/api/html/group__select__next__open__question.html": 6182628715898661160, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_dt_accessor.py": 2229616362327032253, + "venv/lib/python3.13/site-packages/_pytest/py.typed": 15130871412783076140, + "backend/src/core/auth/repository.py": 15675496219240601253, + "venv/lib/python3.13/site-packages/pydantic/_internal/_dataclasses.py": 15256193972947054890, + "docs/api/html/group____serialize__empty__clarification__state.html": 4887954579013674680, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_subclass.py": 11096337912872573314, + "docs/api/html/group__SupersetAccountLookupResponse.html": 3758644365858317477, + "docs/api/html/group__handleBulkDelete__Function.html": 2943774690774667491, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/methods.py": 8059372834301557565, + "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pyi": 14150710436003340771, + "docs/api/html/group__merge__spec.html": 1755088978549667893, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/locators.py": 7638945613579396718, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/connection.py": 5582413226288987808, + "venv/lib/python3.13/site-packages/pip/_internal/models/candidate.py": 3099785363867247334, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_ndarray.py": 6863874753941288585, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/baked.py": 6911138208603240867, + "venv/lib/python3.13/site-packages/httpcore/_sync/__init__.py": 7676937747094511752, + "venv/lib/python3.13/site-packages/pyasn1/type/error.py": 1479878696626082697, + "venv/lib/python3.13/site-packages/fastapi/param_functions.py": 14585101566697773072, + "venv/lib/python3.13/site-packages/pandas/io/excel/_xlrd.py": 14430937200583666658, + "docs/api/html/group__StageExecutionResult.html": 7320596610838714730, + "frontend/src/routes/tools/debug/+page.svelte": 15220194081518936566, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/itsdangerous/url_safe.py": 13454753900702274266, + "venv/lib/python3.13/site-packages/pygments/lexers/prolog.py": 599539463980699865, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py": 17520043776344667309, + "venv/lib/python3.13/site-packages/yaml/serializer.py": 1783761365703703558, + "docs/api/html/group__estimate__row__tokens.html": 6396881984546758852, + "docs/api/html/group__test__idle__state.html": 1062794876151599472, + "venv/lib/python3.13/site-packages/greenlet/tests/test_contextvars.py": 6077482318964702132, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufuncs.py": 12732599294816239111, + "docs/api/html/group__test__log__persistence.html": 9322876502081854485, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptography.py": 4383854709657749855, + "venv/lib/python3.13/site-packages/fastapi/encoders.py": 1914192268055730299, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/RECORD": 18314788057673509075, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.py": 893743087939984792, + "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/licenses/LICENSE": 6808958893921859106, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/parameters.py": 5104015814292812973, + "docs/api/html/group____quote__identifier.html": 856007842378984713, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/decoder.py": 15336592439379775543, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/misc/extended_precision.pyi": 6541258584648209609, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/__init__.py": 11283047029180207588, + "docs/api/html/group__delete__job.html": 992976939561111352, + "venv/lib/python3.13/site-packages/cffi/_shimmed_dist_utils.py": 4824806157907607487, + "docs/api/html/group__DatasourceMetadataService.html": 6885799880168091624, + "venv/lib/python3.13/site-packages/pygments/lexers/dns.py": 17149871769553794654, + "backend/src/api/routes/__tests__/test_dataset_review_api.py": 9392418725327519178, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_aarch64_gcc.h": 8140470539757491899, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_xs.py": 11073752361562791165, + "docs/api/html/group____process__states__for__end.html": 17185750365367165889, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_validate.py": 12789481642730855139, + "venv/lib/python3.13/site-packages/websockets/extensions/base.py": 2457233337788783873, + "frontend/src/routes/admin/users/+page.svelte": 6187566471376083204, + "frontend/build/_app/immutable/chunks/Ba44AqtN.js": 11799296248978509922, + "venv/lib/python3.13/site-packages/PIL/_binary.py": 14039167720582222043, + "docs/api/html/group__GitE2E.html": 5144675262585131661, + "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptodome.py": 15753279711312689193, + "docs/api/html/group__ParsedNativeFilters.html": 9224788944366782259, + "backend/src/plugins/translate/__tests__/test_scheduler.py": 5715885061128238668, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_subclassing.py": 17318983445227658412, + "venv/lib/python3.13/site-packages/pandas/tests/extension/conftest.py": 16886862391161243448, + "venv/lib/python3.13/site-packages/numpy/f2py/capi_maps.pyi": 10638891136586198429, + "docs/api/html/group__frontend__src__services__storageService__js____downloadFile.html": 13909418440035321568, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_nat.py": 18428782698385974380, + "backend/src/services/maintenance/_dashboard_scanner.py": 7573065314352625371, + "venv/lib/python3.13/site-packages/pycparser/_c_ast.cfg": 2649342580905806389, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arraysetops.py": 10588903945244480732, + "venv/lib/python3.13/site-packages/pydantic/validators.py": 8292819706488319845, + "docs/api/html/group__SearchPlugin.html": 13020517702009127534, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/unistring.py": 18037463865539157041, + "venv/lib/python3.13/site-packages/pygments/lexers/d.py": 18120801227801223376, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nested_sequence.pyi": 11245460325970663922, + "venv/lib/python3.13/site-packages/pygments/lexers/bare.py": 18266858932333619451, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/unitofwork.py": 3177255974394221645, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/WHEEL": 16784970174376303810, + "venv/lib/python3.13/site-packages/pygments/lexers/haskell.py": 8011341851439354724, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/comparisons.pyi": 4158883009780474220, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_overlaps.py": 2328652590379935311, + "backend/alembic/script.py.mako": 7041141123739810216, + "docs/api/html/globals_d.html": 2840545806846753350, + "venv/lib/python3.13/site-packages/httpx/_client.py": 1063593889704982038, + "venv/lib/python3.13/site-packages/pygments/__main__.py": 13200500591994479045, + "docs/api/html/group__test__reload__validation__policy__removes__job__when__policy__gone.html": 2311098519001920581, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_printing.py": 1901396869745313303, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/conftest.py": 12368472226536104037, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_impl.py": 15673691977157706075, + "docs/api/html/group__TranslateJobsApi.html": 7339024353697591080, + "venv/lib/python3.13/site-packages/greenlet/tests/fail_initialstub_already_started.py": 5562030563802006923, + "backend/tests/test_core_scheduler.py": 2305397851769292578, + "docs/api/html/group__setupTests__Module.html": 4447681325358958988, + "venv/lib/python3.13/site-packages/pandas/tests/test_errors.py": 10116716310156374885, + "docs/api/html/group__TranslationRunLanguageStats.html": 6103116714421346031, + "backend/src/core/task_manager/__tests__/test_task_logger.py": 2358230637213309608, + "venv/lib/python3.13/site-packages/pygments/lexers/css.py": 517586374264113606, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/__init__.py": 1948986914452928735, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.c": 9134176670140958459, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_reductions.py": 1713831062142840146, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/missing.py": 11241703105777668183, + "backend/tests/services/clean_release/test_report_audit_immutability.py": 8871167753190671337, + "backend/src/plugins/translate/dictionary_import_export.py": 12639118203353044308, + "venv/lib/python3.13/site-packages/passlib/utils/binary.py": 13123975436674239306, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_pyf_src.py": 12351476647369971852, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.pyi": 17469792319379909312, + "docs/api/html/group____update__confirmation__state.html": 4308473508764835316, + "venv/lib/python3.13/site-packages/pyasn1/type/tagmap.py": 14075859042659927673, + "venv/lib/python3.13/site-packages/numpy/rec/__init__.pyi": 1782063765067823837, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_common.py": 9097104320321411408, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/precision.f90": 5518911932373565642, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/assertion_session.py": 18109848200936213195, + "backend/src/services/profile_service.py": 15545850721840177094, + "frontend/src/lib/stores/taskDrawer.js": 16349119158364586930, + "venv/lib/python3.13/site-packages/websockets/typing.py": 13268801935274185287, + "docs/api/html/group__APIClient______init____.html": 16760764157004706288, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/validation": 5398883285871046811, + "venv/lib/python3.13/site-packages/pip/_internal/distributions/base.py": 16714704323747942110, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/__init__.py": 8811477458865810434, + "docs/api/html/group__test__base__dir__constant__replaces__parents.html": 16003639855862888658, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapper.py": 16715864918034312591, + "venv/lib/python3.13/site-packages/pandas/_libs/writers.pyi": 10494773026133244183, + "docs/api/html/group__test__get__dashboards__pagination__zero__page.html": 9231270374996443556, + "docs/api/html/group__TestBuildReportQueryString__Class.html": 12539253061507680489, + "venv/lib/python3.13/site-packages/pydantic/_internal/_typing_extra.py": 14843908493613991959, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/char.f90": 12203871998988759364, + "venv/lib/python3.13/site-packages/passlib/utils/compat/__init__.py": 17369882236606423129, + "venv/lib/python3.13/site-packages/attrs/exceptions.py": 2871247302586505373, + "docs/api/html/group____clear__assistant__state.html": 17985750078223833333, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py": 15077617224435782028, + "docs/api/html/group__ValidationTaskList.html": 10113816788777682664, + "venv/lib/python3.13/site-packages/pandas/core/groupby/grouper.py": 18110568057754450953, + "venv/lib/python3.13/site-packages/httpcore/py.typed": 15130871412783076140, + "backend/git_repos/remote/test-repo.git/hooks/pre-rebase.sample": 15693225890270078290, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py": 10894229569442891394, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____resetTranslationRun.html": 7263960980597630853, + "docs/api/html/group__SupersetClientGetDatabases.html": 14956619577295357507, + "frontend/src/routes/admin/settings/llm/+page.svelte": 17653948269643098768, + "docs/api/html/group__handleCreateMapping__Function.html": 12147099060978125723, + "venv/lib/python3.13/site-packages/gitdb/test/test_base.py": 16305779366785148795, + "docs/api/html/group__ResourceServiceModule.html": 7820716333589136956, + "docs/api/html/group__delete__role.html": 8546807030559673692, + "docs/api/html/group__DatabaseResponse.html": 9450948146278252865, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/models.py": 5656065513234782067, + "venv/lib/python3.13/site-packages/_pytest/subtests.py": 14573790885130724739, + "docs/api/html/group__SupersetClientNormalizeEffectiveFiltersForQueryContext.html": 14678121546228653746, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/__init__.py": 4870269250323682576, + "frontend/playwright-report/trace/uiMode.C2Efnu2P.js": 2717171022864487300, + "docs/api/html/group__delete__session.html": 5324305490692827144, + "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimes.py": 14331984919364185787, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/evaluator.py": 6348022789899831255, + "venv/lib/python3.13/site-packages/gitdb/db/ref.py": 1176701624460337371, + "frontend/src/routes/datasets/__tests__/bulk_actions.test.js": 8445672988319354987, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.APACHE": 11041304845352917971, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.pyi": 3145980717589050713, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/__init__.py": 17454177076687312424, + "docs/api/html/group__StorageModels.html": 7704378084745051491, + "docs/api/html/group__list__plugins.html": 1922104558299502422, + "docs/api/html/group__ArtifactType.html": 5232079852731603995, + "frontend/src/components/StartupEnvironmentWizard.svelte": 17705330033482916904, + "venv/lib/python3.13/site-packages/passlib/utils/__init__.py": 11704122205941631503, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_transform.py": 18159263102656491974, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_reductions.py": 14432782234851468060, + "backend/src/plugins/translate/_batch_insert.py": 10448320504073856278, + "docs/api/html/group__NotificationProvider.html": 9879076556154479190, + "docs/api/html/globals_func_t.html": 17227441317231061430, + "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/RECORD": 7914594107698271488, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/__init__.py": 3969805865048730851, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/test_nanops.py": 8709105600366913011, + "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.pyi": 5892973170789971535, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_extension.py": 17245018204352082978, + "docs/api/html/group__DeploymentEnvironment.html": 16685176550591536655, + "dist/docker/ss-tools-frontend.0.1.8.tar.xz": 978168711641698755, + "docs/api/html/group__NativeFilterExtractionTests.html": 14602614888501976510, + "docs/api/html/group__translate__router.html": 11232553803187505597, + "docs/api/html/group__pop__span.html": 10448187202370662718, + "docs/api/html/group__NetworkModule.html": 8343794053715962544, + "docs/api/html/group__handleValidate__Function.html": 2317767928210351135, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py": 11139432164346742421, + "docs/api/html/group__list__conversations.html": 9235216586392560543, + "docs/api/html/group__test__no__active__events.html": 18077367180380066108, + "docs/api/html/group__build__default__preference.html": 9555424369113315940, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_qcut.py": 8972418112914954758, + "docs/api/html/group__handleEdit__Function.html": 15858142229042020143, + "docs/api/html/group__frontend__src__services__adminService__js____deleteUser.html": 16139969298187481622, + "venv/lib/python3.13/site-packages/pandas/tests/config/test_config.py": 4630144191929135452, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh17859.f": 13562390882286135024, + "docs/api/html/group__WebSocketLogHandler______init____.html": 6496043986001513744, + "docs/api/html/group__MaintenanceDashboardResult.html": 796312223553899991, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/licenses/LICENSE": 17020744174398828059, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit.py": 15498810174380798405, + "venv/lib/python3.13/site-packages/passlib/utils/md4.py": 4521366665754877351, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_generic.py": 1297407268239966429, + "venv/lib/python3.13/site-packages/pandas/tests/base/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/vector.py": 9941185556118343263, + "docs/api/html/group__get__run__history.html": 15431464627723325766, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/cast.py": 342228968652072052, + "venv/lib/python3.13/site-packages/pandas/compat/_constants.py": 3909166275649444824, + "venv/lib/python3.13/site-packages/pygments/lexers/dotnet.py": 17970884907837213099, + "docs/api/html/group__AsyncAPIClient____handle__network__error.html": 14286771511204835103, + "docs/api/html/group__test__extract__imported__filters__preserves__clause__level__native__filter__payload__for__preview.html": 14624980795486649087, + "venv/lib/python3.13/site-packages/pandas/_libs/hashing.pyi": 5241441378073929968, + "docs/api/html/group__test__resolve__trusted__policy__snapshots__rejects__override__attempt.html": 17876948714761629910, + "venv/lib/python3.13/site-packages/jsonschema/_format.py": 8987698648383779883, + "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_tests.cpython-313-x86_64-linux-gnu.so": 442168371609469538, + "venv/lib/python3.13/site-packages/multipart/exceptions.py": 8580522204565816132, + "backend/git_repos/remote/test-repo.git/refs/heads/dev": 8596267258668462395, + "docs/api/html/group__MaintenancePackage.html": 10072412896629514555, + "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/WHEEL": 691462085217888236, + "docs/api/html/group__test__get__repository__status__batch__deduplicates__and__truncates__ids.html": 17068511664214583355, + "docs/api/html/group__ActivityTestModule.html": 550457699042373901, + "venv/lib/python3.13/site-packages/rapidfuzz/process_py.py": 17563673072076876125, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/WHEEL": 13347410390513723930, + "docs/api/html/group__DatasetColumn.html": 6676823301052927790, + "docs/api/html/group__test__creates__new__banner.html": 4106644046947123822, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__frontend__src__types__backup__ts____Backup.html": 5424115272360107965, + "docs/api/html/group__requestDatasetReviewApi__Function.html": 8663890311322961011, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_dtypes.py": 13065857166166716628, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufunclike.pyi": 12215374902556647369, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_series.py": 2980850649986213051, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timezones.py": 1962830142478494009, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/profiling.py": 16030397164203895281, + "venv/lib/python3.13/site-packages/pygments/formatters/latex.py": 7514533384468139434, + "docs/api/html/group__TestPublicationService.html": 8868260289891747694, + "venv/lib/python3.13/site-packages/pygments/styles/default.py": 13992125975366109635, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/licenses/LICENSE": 3868190977070717994, + "docs/api/html/group____make__preview__ready__session.html": 4654960038289930410, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayobject.py": 13337664596833150958, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/WHEEL": 5160964596297665692, + "docs/api/html/group__AppTimezone.html": 1905496000801051257, + "venv/lib/python3.13/site-packages/pygments/styles/rainbow_dash.py": 12236746494379891602, + "backend/src/plugins/translate/service_target_schema.py": 7080510961466191005, + "docs/api/html/group__TestTaskPersistenceService.html": 12922838788856571301, + "venv/lib/python3.13/site-packages/numpy/fft/tests/test_helper.py": 5362180645056413158, + "docs/api/html/group__derive__readiness__state.html": 15658475287495832903, + "venv/lib/python3.13/site-packages/pip/_internal/utils/unpacking.py": 106471890743211548, + "venv/lib/python3.13/site-packages/numpy/lib/format.py": 14971167445246135440, + "frontend/src/lib/api.js": 3212081782177148601, + "frontend/src/lib/components/reports/ReportCard.svelte": 7009840605870276729, + "docs/api/html/group____map__candidate__provenance.html": 2905607334694773753, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/METADATA": 6557531107125553027, + "docs/api/html/group__orchestrator__run__completion.html": 9161921145624746021, + "docs/api/html/group__DatasetReviewWorkspaceEntry.html": 10861049542091300011, + "backend/src/core/migration/risk_assessor.py": 4161499861941446814, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/errors.py": 17581857895562494222, + "venv/lib/python3.13/site-packages/pydantic/v1/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/_pytest/nodes.py": 515939386237558331, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_label_or_level_utils.py": 3423644585913150841, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f77fixedform.f95": 10898637805043867370, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/RECORD": 3283324710045856319, + "venv/lib/python3.13/site-packages/_pytest/unittest.py": 10848803633530338204, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_api.py": 16743710460729009914, + "docs/api/html/group__DatasetRunContext.html": 16196372668932517376, + "venv/lib/python3.13/site-packages/PIL/WalImageFile.py": 12442826195371137238, + "backend/src/services/git/_gitea.py": 13718280051830211819, + "venv/lib/python3.13/site-packages/pandas/core/indexes/interval.py": 17965911911189276907, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_manylinux.py": 12272275001961360101, + "venv/lib/python3.13/site-packages/PIL/WebPImagePlugin.py": 5378349556563611996, + "docs/api/html/group__test__orchestrator__start__session__bootstraps__recovery__state.html": 13152006356907717305, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/ec_key.py": 5961540064444749833, + "venv/lib/python3.13/site-packages/pydantic/v1/fields.py": 3948219773415054006, + "venv/lib/python3.13/site-packages/numpy/linalg/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_strings.py": 17398806130402098912, + "backend/src/core/migration_engine.py": 4215834818829537236, + "docs/api/html/group__insert__banner__markdown__at__top.html": 2314550343350178265, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro_py.py": 11991492232381887787, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py": 2569618488804767890, + "docs/api/html/group__GitServiceBranchMixin.html": 12313083573663321996, + "docs/api/html/group__unlock__field__semantic.html": 6237915006750814401, + "backend/src/plugins/translate/orchestrator_validation.py": 17464651621865127561, + "frontend/build/_app/immutable/assets/AssistantChatPanel.D4L5jlt0.css": 10724144138366384732, + "venv/lib/python3.13/site-packages/httpcore/_backends/sync.py": 576247220834179928, + "frontend/src/routes/storage/+page.svelte": 5003283673039328108, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odf.py": 15484940480924122966, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/__init__.py": 4781700006554860887, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_append.py": 17016125916761365423, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/theme.py": 14225878259183479398, + "venv/lib/python3.13/site-packages/gitdb/db/git.py": 13552203494737962026, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/qt.py": 12828696264909048448, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/module_data_docstring.f90": 18245403117623015312, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_localize.py": 18409231586451494605, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_snap.py": 4515844385819802784, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/__init__.py": 12975130533860125272, + "docs/api/html/group__CotJsonFormatter__format.html": 824540483579618600, + "venv/lib/python3.13/site-packages/gitdb/utils/__init__.py": 15130871412783076140, + "docs/api/html/group__FeaturesConfig.html": 10248250923166873006, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/diagnose.py": 17275977405061138181, + "docs/api/html/group__EnterpriseCleanSetupE2E.html": 12663560821891997943, + "backend/src/services/clean_release/mappers.py": 16365217670331724280, + "venv/lib/python3.13/site-packages/pip/_internal/commands/debug.py": 13528633965821062950, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/f90continuation.f90": 17056591497459381871, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_other.py": 783701524491896337, + "docs/api/html/group__test__invalid__key__raises__401.html": 17548308712736303847, + "venv/lib/python3.13/site-packages/_pytest/compat.py": 10046464101087327473, + "venv/lib/python3.13/site-packages/urllib3/contrib/emscripten/__init__.py": 3619091571279924576, + "venv/lib/python3.13/site-packages/httpx/_auth.py": 1283189552342958900, + "venv/lib/python3.13/site-packages/pip/_internal/utils/deprecation.py": 7435269308988385470, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING": 14459782230785388765, + "venv/lib/python3.13/site-packages/pygments/lexers/pointless.py": 17552020039609007881, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_features.py": 955759654189150497, + "docs/api/html/group__test__cte__with__schema__tables.html": 5116514654709271953, + "docs/api/html/group__test__manifest__service__rebuild__creates__new__version.html": 15569805611870644295, + "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/RECORD": 7363618870628306370, + "venv/lib/python3.13/site-packages/pandas/io/_util.py": 13608922148814398500, + "docs/api/html/group__save__run__context.html": 10013203780160130788, + "venv/lib/python3.13/site-packages/cryptography/hazmat/_oid.py": 755042927629281577, + "venv/lib/python3.13/site-packages/pandas/io/excel/__init__.py": 6662687963241471393, + "docs/api/html/group__getDatasetReviewConflictMessage__Function.html": 969336450247771573, + "venv/lib/python3.13/site-packages/pygments/lexers/ride.py": 14096748896451437594, + "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.py": 546976622482819095, + "venv/lib/python3.13/site-packages/pygments/styles/__init__.py": 18383248976818885498, + "docs/api/html/group__DatasetReviewFindingModels.html": 8713463567973015699, + "docs/api/html/group__ManifestRepositoryModule.html": 16790910808753143759, + "docs/api/html/group__DashboardHub__handleTemporaryShowAll__Function.html": 4145528349117048216, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_constructors.py": 16197656939536784142, + "docs/api/html/group____other__admin__user.html": 12434910978826868451, + "frontend/build/_app/immutable/chunks/CbSlUFiI.js": 14704044121881920915, + "docs/api/html/group__TranslateJobUpdate.html": 3630855679853267875, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo90.f90": 2435648607844815947, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_transpose.py": 1686867045869336370, + "venv/lib/python3.13/site-packages/pygments/lexers/smv.py": 8253688483040957894, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_map.py": 1115471561925426082, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x25519.py": 18229054440703471493, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_julian_date.py": 850085757774959003, + "venv/lib/python3.13/site-packages/referencing/tests/test_jsonschema.py": 9815500901482570619, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_use.f90": 2657057181044959432, + "venv/lib/python3.13/site-packages/cryptography/x509/certificate_transparency.py": 11337143186866439208, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_indexing.py": 17106010337164216722, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_hist_method.py": 2109257663592928, + "venv/lib/python3.13/site-packages/numpy/_globals.py": 8736093011628027941, + "docs/api/html/group____validate__environment.html": 5948694607401903979, + "frontend/eslint.config.js": 14296985543214702398, + "venv/lib/python3.13/site-packages/authlib/consts.py": 14226604841596736870, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/RECORD": 2688648742701423549, + "venv/lib/python3.13/site-packages/referencing/tests/test_referencing_suite.py": 8875120876162738440, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np126.pkl.gz": 9677994062701347206, + "backend/src/core/utils/superset_context_extractor/_filters.py": 14230663187221307078, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/data.f90": 13833561828029901615, + "venv/lib/python3.13/site-packages/fastapi/websockets.py": 9910052969532075719, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_typing.py": 10311989961588249147, + "docs/api/html/group__setLoading__Function.html": 3715890994479237152, + "docs/api/html/group____resolve__candidate__and__report.html": 16524530636169236472, + "docs/api/html/group__MaintenanceComponentTests.html": 2439633927977952953, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period.py": 9715843209581883915, + "docs/api/html/group__test__event__not__found.html": 3590102006915597365, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/scoping.py": 18223258186431020473, + "docs/api/html/group__PublicationRecord.html": 12219741124071026101, + "venv/lib/python3.13/site-packages/pydantic_settings/exceptions.py": 18300876747412267999, + "docs/api/html/group__get__maintenance__settings.html": 68073512936304627, + "venv/lib/python3.13/site-packages/pygments/lexers/parsers.py": 12372594608244797844, + "backend/src/models/profile.py": 12624256233571777563, + "docs/api/html/group__test__dataset__review__scoped__command__returns__confirmation__for__mapping__approval.html": 2557882480884590809, + "venv/lib/python3.13/site-packages/pip/_internal/index/package_finder.py": 12631116673181479779, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/take.py": 3170415198452493008, + "docs/api/html/globals_q.html": 11227781208567450336, + "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp.py": 16797760903996739079, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_ops.py": 16346030307660048773, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/utils.py": 8633356372647820801, + "venv/lib/python3.13/site-packages/rpds/rpds.cpython-313-x86_64-linux-gnu.so": 561326734302492859, + "docs/api/html/group__toggleAssistantChat__Function.html": 16851022063245577373, + "docs/api/html/group__DatasetsResponse.html": 4984654878449311988, + "docs/api/html/group____load__from__legacy__file.html": 14273299865611176783, + "backend/src/services/__tests__/test_resource_service.py": 7247551330585718683, + "docs/api/html/search/functions_c.js": 2394026458529255260, + "venv/lib/python3.13/site-packages/pandas/core/ops/docstrings.py": 3721650738518391256, + "docs/api/html/group__TaskGraphModule.html": 1612918400039070240, + "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/RECORD": 17594742868855466393, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_timedeltas.py": 4118671843063545221, + "venv/lib/python3.13/site-packages/pandas/_libs/sas.cpython-313-x86_64-linux-gnu.so": 16309838137741124637, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers.py": 4940305787631493030, + "backend/src/plugins/git/__init__.py": 2144831474780401625, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/functions.py": 16567226076808693246, + "venv/lib/python3.13/site-packages/pandas/_libs/groupby.cpython-313-x86_64-linux-gnu.so": 14931394220112658268, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_array_from_pyobj.py": 9596224861290975079, + "docs/api/html/group__SupersetCompilationAdapter__imports.html": 7332800023400610712, + "docs/api/html/group__get__environments.html": 1082546386773301101, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_utils.py": 8668650648802195016, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/operators.py": 2468046993343796128, + "backend/src/services/__tests__/test_encryption_manager.py": 11651916791559202800, + "venv/lib/python3.13/site-packages/pydantic/_internal/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic/class_validators.py": 4643597082531287188, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/implicit.py": 5400555157488808948, + "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.py": 6205675182996213576, + "venv/lib/python3.13/site-packages/numpy/random/_mt19937.pyi": 15904463104068500379, + "venv/lib/python3.13/site-packages/numpy/fft/_helper.pyi": 9546918341939102632, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_misc.py": 16393451200513733144, + "backend/src/services/clean_release/repositories/__init__.py": 10330511711442503831, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_timezones.py": 4771674179164304731, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/WHEEL": 13232065379159720345, + "backend/tests/test_dashboards_api.py": 18340150083350981192, + "venv/lib/python3.13/site-packages/numpy/_distributor_init.pyi": 1308368231471324581, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/dml.py": 17308908394336670576, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/extending.py": 9859383893070793914, + "docs/api/html/group__TranslationExecutor.html": 4954564748951736491, + "docs/api/html/group__ApiRoutesGetAttr.html": 10833820970838947139, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_deprecations.py": 16124509063246256847, + "venv/lib/python3.13/site-packages/pandas/compat/_optional.py": 12714208019736797073, + "docs/api/html/group__MaintenanceStartResponse.html": 16246400810680453876, + "docs/api/html/group__replacer.html": 8138915867492512587, + "docker/nginx.ssl.conf": 12350396037548059686, + "docs/api/html/group____flush__logs.html": 17794969687461313535, + "docs/api/html/group__getReportTypeProfile__Function.html": 16691315682732378844, + "docs/api/html/group__test__datasets__api.html": 13468016019586118937, + "venv/lib/python3.13/site-packages/fastapi/middleware/__init__.py": 5289703478893407762, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/__init__.py": 11121941644106061453, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_is_monotonic.py": 12899848089975919515, + "docs/api/html/group__frontend__src__services__taskService__js____clearTasks.html": 16739125797009486352, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/resources.py": 11533706401259147051, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_overrides.py": 92685611709898742, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/unicode_comment.f90": 11326797716215117266, + "venv/lib/python3.13/site-packages/pandas/io/feather_format.py": 13242733585787888812, + "backend/src/plugins/translate/_utils.py": 10032606591729209441, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/base.py": 16891437410462671317, + "frontend/src/app.html": 10207534912546549080, + "docs/api/html/group__ReviewWorkspaceHeader.html": 7831080431402796410, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/base.py": 11386927749822539057, + "docs/api/html/group__TestStorageConfigMigration.html": 18428544366449284343, + "docs/api/html/group__GitDeps.html": 16624578576304918458, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dropna.py": 3168969662216766293, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_values.py": 834951665649981397, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/plugin.py": 18006138132596504101, + "venv/lib/python3.13/site-packages/sqlalchemy/util/queue.py": 1289234691048379395, + "frontend/src/lib/api/reports.js": 12520710039739782171, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/chararray.pyi": 9683985931409244216, + "venv/lib/python3.13/site-packages/numpy/random/__init__.py": 11732100564412485633, + "docs/api/html/group____validate__provider.html": 10145375276233333598, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_check_indexer.py": 2929031395357057009, + "docs/api/html/group__SetupTestsModule.html": 5544696171487564620, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py": 15735839022111230173, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_between_time.py": 14266333058913745751, + "backend/src/api/routes/assistant/_tool_capabilities.py": 17042908584051537683, + "docs/api/html/group__test__get__report__detail__not__found.html": 11700510579759808202, + "venv/lib/python3.13/site-packages/pygments/lexers/_lilypond_builtins.py": 1766946089314631488, + "docs/api/html/group__TestDemoModeIsolation.html": 16048106037006305955, + "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.pyi": 15509415708991012734, + "venv/lib/python3.13/site-packages/pygments/lexers/_stan_builtins.py": 1961875544235948177, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi": 16042272072158438261, + "frontend/build/_app/immutable/nodes/25.CAemtcXS.js": 8793927375418754513, + "backend/src/plugins/translate/preview_prompt_helpers.py": 17075647076700392388, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_unique.py": 3989602816674881215, + "venv/lib/python3.13/site-packages/numpy.libs/libscipy_openblas64_-096271d3.so": 14338208504131470036, + "docs/api/html/group__TaskDrawerTestModule.html": 15361427361837653112, + "venv/lib/python3.13/site-packages/pydantic/_internal/_git.py": 8760779092674434712, + "frontend/src/lib/api/translate/dictionaries.js": 13928848549982189213, + "venv/lib/python3.13/site-packages/pandas/io/excel/_xlsxwriter.py": 16873939324418752965, + "docs/api/html/group__CleanReleaseV2ReleaseApiTests.html": 394913399748993155, + "backend/src/api/routes/dashboards/_helpers.py": 13624740316332136004, + "backend/src/api/routes/clean_release_v2.py": 7430240321470425220, + "docs/api/html/group__FilterState.html": 14518206927080029563, + "docs/api/html/group__DashboardActionRoutes.html": 7575118008410498480, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/attr/validators.py": 4453080662115911998, + "docs/api/html/group__status__to__report__status.html": 9738740379403112359, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_orm_types.py": 5508682532707462217, + "docs/api/html/group__docker__frontend__entrypoint__install__ca__certificates.html": 8734321705788517662, + "docs/api/html/group__frontend__src__services__taskService__js____getTaskLogs.html": 14401072635225830595, + "venv/lib/python3.13/site-packages/click/__init__.py": 9261629899056870564, + "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_legacy.py": 9226692984248279094, + "docs/api/html/group__add__logs.html": 9269565121918575356, + "venv/lib/python3.13/site-packages/pygments/lexers/dsls.py": 11696359794961833087, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/METADATA": 10220680536734745103, + "venv/lib/python3.13/site-packages/jose/backends/rsa_backend.py": 13507186353881931758, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.pyi": 17528421279543420867, + "venv/lib/python3.13/site-packages/numpy/core/fromnumeric.py": 2136433423902202138, + "docs/api/html/group__list__reports.html": 7378159237170020810, + "docs/api/html/group__test__parse__superset__link__dashboard__permalink__recovers__dataset__from__nested__dashboard__state.html": 16106300672165677177, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/RECORD": 11604408378998955169, + "venv/lib/python3.13/site-packages/pandas/tests/test_col.py": 14781555956509956371, + "docs/api/html/group__stage__result__map.html": 18249886114510031003, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/styled.py": 8726407298858811635, + "docs/api/html/group__ModelsPackage.html": 13346504466563987278, + "venv/lib/python3.13/site-packages/pygments/styles/lilypond.py": 12486578704132023691, + "backend/src/plugins/translate/_llm_http.py": 13727901565339897028, + "venv/lib/python3.13/site-packages/pillow.libs/libXau-154567c4.so.6.0.0": 4340972632724893052, + "docs/api/html/group__UpdateSessionRequest.html": 6044024401135206762, + "docs/api/html/group__check__target__schema.html": 16422257862833670253, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/meson.build": 8784756284679859367, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/content": 12465123883296161860, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_reductions.py": 17810094294132819208, + "frontend/src/lib/components/layout/Breadcrumbs.svelte": 8720668241659707399, + "backend/src/api/routes/assistant/_tool_task_status.py": 600224968391778009, + "docs/api/html/group__connect__Function.html": 12882031631631590225, + "docs/api/html/group__DeleteRunningTasksUtil.html": 11213940847453331921, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____resetSession.html": 1485006596273058906, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/__init__.py": 348469143777130970, + "venv/lib/python3.13/site-packages/pandas/core/computation/api.py": 17919983681396223380, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_to_julian_date.py": 6868080396693831272, + "docs/api/html/group__handleCommit__Function.html": 15953199697396659260, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/core": 6622984507059115517, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/client.py": 17305914656270781911, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/METADATA": 3786116428839780263, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/client_auth.py": 3301527187487914220, + "docs/api/html/group__update__validation__policy.html": 13352530740640222844, + "docs/api/html/search/all_9.js": 16141436105417826748, + "venv/lib/python3.13/site-packages/jaraco/functools/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/content": 16257335642847993866, + "backend/src/services/rbac_permission_catalog.py": 11280653389166352517, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_timedelta.py": 17052461094717926538, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/signature.py": 3614545297259098566, + "venv/lib/python3.13/site-packages/pycparser/ply/ctokens.py": 1599952198962458730, + "docs/api/html/group__TopNavbarStoreTest__Module.html": 11592246663822811031, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_complex.py": 11732242536199439023, + "docs/api/html/group__TaskResponse.html": 12180333889349074765, + "docs/api/html/group__GitServiceMergeMixin.html": 4354986681189316190, + "docs/api/html/group__ReportTypeProfiles__Module.html": 6048512366307639028, + "docs/api/html/group__now.html": 883313604471122224, + "docs/api/html/group__TuiFacadeAdapter.html": 9778651468218666889, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarinherit.py": 15240535501243623986, + "venv/lib/python3.13/site-packages/pandas/core/window/expanding.py": 14141093310397856286, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_keys.py": 10813898277355150779, + "venv/lib/python3.13/site-packages/pycparser/ply/yacc.py": 4218889705926129841, + "docs/api/html/group____ensure__filter__source__enum__values.html": 15520428780478561060, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_pickle.py": 4216445955812104014, + "venv/lib/python3.13/site-packages/pip/_internal/utils/_jaraco_text.py": 9046755357011730699, + "docs/api/html/group__GitSettingsPageUxTest__Module.html": 7822522085605911446, + "venv/lib/python3.13/site-packages/greenlet/tests/test_version.py": 6317733744899804292, + "venv/lib/python3.13/site-packages/pydantic/v1/class_validators.py": 6471688474853407718, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_multiplier.f": 13432803601367981068, + "venv/lib/python3.13/site-packages/passlib/tests/_test_bad_register.py": 5446848310854472376, + "docs/api/html/group____parsing__imports.html": 14931596898828785688, + "venv/lib/python3.13/site-packages/pip/_internal/network/utils.py": 13421242949994555055, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/__init__.py": 11414161642984633362, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_doc.py": 7865575374100528149, + "docs/api/html/group__create__role.html": 7308394036777191380, + "frontend/src/lib/stores/__tests__/test_sidebar.js": 2607679830387722396, + "docs/api/html/group__no__external__endpoints.html": 4259752722362818397, + "docs/api/html/group__handleMappingUpdate__Function.html": 7438288773532136469, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/securetransport.py": 12104888897121018676, + "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.ux.test.js": 10458366870385391445, + "docs/api/html/group__handleUpdate__Function.html": 5587686760218191810, + "docs/api/html/graph_legend.dot": 5298420021651416491, + "docs/api/html/group__test__parse__superset__link__dashboard__permalink__partial__recovery.html": 2884738583447431524, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_to_xml.py": 8477770807916990981, + "venv/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py": 8145740626773638218, + "docs/api/html/group__frontend__src__services__git__utils__js____isNumericDashboardRef.html": 10032907620430901043, + "docs/api/html/group__environmentContext__Store.html": 12330215193574148972, + "docs/api/html/group__DatasetReviewEnums.html": 8736640618120318797, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/console.py": 10217327186846649646, + "backend/src/scripts/init_auth_db.py": 8910351541713877041, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____mergeCollectionItem.html": 2019932382006674486, + "docs/api/html/group__get__storage__settings.html": 15563164066742946450, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_kind.py": 18178830861475480607, + "docs/api/html/group__get__database__mappings.html": 4882039116942012048, + "venv/lib/python3.13/site-packages/passlib/ext/django/__init__.py": 11713862539944289631, + "venv/lib/python3.13/site-packages/sqlalchemy/types.py": 15819795572817819102, + "venv/lib/python3.13/site-packages/numpy/_typing/_shape.py": 12667553664122376205, + "docs/api/html/group__ToolsService.html": 2114074285056843811, + "docs/api/html/group__remove__empty__directories.html": 14528085660669051293, + "venv/lib/python3.13/site-packages/pip/_internal/cli/autocompletion.py": 7651919329635563739, + "venv/lib/python3.13/site-packages/PIL/__main__.py": 827665918723937817, + "venv/lib/python3.13/site-packages/psycopg2/errors.py": 10441360078612452560, + "venv/lib/python3.13/site-packages/urllib3/_request_methods.py": 13671055114247516883, + "docs/api/html/group__DictionaryCorrectionService.html": 5320595430025547036, + "frontend/src/components/Footer.svelte": 12838918285533328222, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_formats.py": 5259100336852479395, + "frontend/build/_app/immutable/nodes/13.DPL26g_M.js": 7306388041479759955, + "docs/api/html/group__download__skipped__csv.html": 13671264474809862196, + "frontend/tests/maintenance-store.test.ts": 7211673236234440419, + "docs/api/html/group____find__dashboard__id__by__slug.html": 4034569905177392135, + "docs/api/html/group__test__bulk__corrections__atomic.html": 16703462863207899908, + "docs/api/html/group__get__translation__schedules.html": 13478548102530293059, + "docs/api/html/group__LogFilter.html": 10465257837287313695, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_file_handling.py": 732772718210067599, + "backend/src/services/profile_preference_service.py": 8662957068796622512, + "docs/api/html/group__oauth2__scheme__optional.html": 10907588018100611451, + "docs/api/html/group__revoke__publication.html": 13242569991906257496, + "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/WHEEL": 14929202952940710322, + "backend/src/services/maintenance/_banner_renderer.py": 16908851830627729260, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/nested_schemas.py": 4114645644496880787, + "frontend/src/lib/components/layout/__tests__/sidebarNavigation.test.js": 1278883813018181993, + "docs/api/html/group__ComplianceReport.html": 8461864359499297379, + "docs/api/html/globals_func_f.html": 11627460573470994982, + "venv/lib/python3.13/site-packages/pyasn1/type/base.py": 6801504886392649787, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/framework_integration.py": 3935346152156572025, + "docs/api/html/group__LLMReportPage.html": 1528192812986476498, + "docs/api/html/group__ClarificationSessionSummaryResponse.html": 6287729552566678011, + "docs/api/html/group__frontend__src__services__git__utils__js____extractHttpHost.html": 17505817962163768324, + "venv/lib/python3.13/site-packages/fastapi/openapi/constants.py": 1111838716953068285, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/expression.py": 1248029135284283479, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/__init__.py": 6327819603201859020, + "backend/src/core/superset_profile_lookup.py": 7466572583388472449, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_align.py": 10284128457194204574, + "venv/lib/python3.13/site-packages/fastapi/exceptions.py": 12665775976182239091, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/token_endpoint.py": 6118770923176099889, + "docs/api/html/group______tests______test__task__logger.html": 3022509304089562027, + "venv/lib/python3.13/site-packages/pandas/core/strings/__init__.py": 13025387151657797130, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/conftest.py": 2643915042664055969, + "venv/lib/python3.13/site-packages/pygments/lexers/berry.py": 4540479522141160472, + "docs/api/html/group__ProfilePreferenceResponse.html": 14962554366043501331, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/gcp.py": 12328409672947966867, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_character/foo77.f": 4046503346951676582, + "venv/lib/python3.13/site-packages/pandas/_libs/lib.cpython-313-x86_64-linux-gnu.so": 6452315003728497116, + "venv/lib/python3.13/site-packages/pygments/formatters/img.py": 18230180171537565208, + "frontend/playwright-report/trace/defaultSettingsView.BDKsFU3c.css": 12935551588108679759, + "frontend/playwright-report/data/6d0114df37cfae2e2f6fda7704f703c2ddcbd1db.webm": 6248292794123488182, + "docs/api/html/group____plan__dataset__review__intent.html": 16025749447796355247, + "venv/lib/python3.13/site-packages/pandas/io/parsers/__init__.py": 11752833703934347122, + "backend/src/plugins/translate/service_utils.py": 17789004018368061334, + "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__update__markdown__chart.html": 1789011860209290209, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/parameters.py": 12219404446503940066, + "docs/api/html/group__test__create__job__invalid__upsert__strategy.html": 1478408184279931764, + "venv/lib/python3.13/site-packages/_pytest/monkeypatch.py": 2635774457434545573, + "backend/src/schemas/dataset_review_pkg/_dtos.py": 12924172044908218354, + "docs/api/html/group__get__native__filter__state__async.html": 204597673029712023, + "docs/api/html/group__ProviderConfig.html": 14874415699044736742, + "docs/api/html/group__ReviewWorkspaceLeftSidebar.html": 17632781333650288823, + "backend/src/plugins/translate/__tests__/__init__.py": 2490542523242946173, + "venv/lib/python3.13/site-packages/pygments/lexers/ecl.py": 12886695201994337568, + "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.py": 2215312866861772848, + "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector.py": 15458248567257759500, + "venv/lib/python3.13/site-packages/numpy/core/_multiarray_umath.py": 10973016138581934035, + "docs/api/html/group__TaskLogViewer.html": 7478297409067123926, + "docs/api/html/group__ManifestBuilder.html": 9042925776836812751, + "docs/api/html/group__delete__run.html": 15832347541244359990, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/flow_control.py": 4822804840057360506, + "venv/bin/websockets": 12099863324306500376, + "docs/api/html/group__BulkReplaceResponse.html": 9373003844931059590, + "frontend/src/routes/datasets/[id]/+page.svelte": 7539252327680602221, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_hour.py": 13754646634123368797, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py": 3042005457138956499, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/containers.py": 5969831151685016772, + "docs/api/html/group__test__throttled__scheduler.html": 9291093931746926808, + "venv/lib/python3.13/site-packages/starlette/middleware/wsgi.py": 12506262030845301606, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_utils.py": 4612923732868929770, + "frontend/e2e/tests/smoke.e2e.js": 11376448043256596939, + "venv/lib/python3.13/site-packages/git/objects/submodule/__init__.py": 12958523219496107878, + "venv/lib/python3.13/site-packages/pluggy/_hooks.py": 6941854999025808196, + "venv/lib/python3.13/site-packages/pygments/lexers/sql.py": 15566262873151658034, + "venv/lib/python3.13/site-packages/numpy/f2py/setup.cfg": 17974180080760231128, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_rolling.py": 11118160360072677569, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_fillna.py": 7244491410004453166, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/_typing.py": 17598638528781632158, + "venv/lib/python3.13/site-packages/PIL/__init__.py": 10393984345353752724, + "docs/api/html/group__ManifestItem.html": 17134684545331977423, + "venv/lib/python3.13/site-packages/pandas/tests/frame/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/strings.py": 3623885046208831106, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_astype.py": 10171766562093740708, + "docs/api/html/navtree.js": 13579380893835360126, + "docs/api/html/group__test__get__dashboards__with__search.html": 9037019464046043095, + "venv/lib/python3.13/site-packages/py.py": 16248211404003181569, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/json.py": 4660603129768960095, + "docs/api/html/group__ProfileSchemas.html": 17384165309593912417, + "frontend/build/_app/immutable/nodes/9.CHB8pABR.js": 5896131460637769578, + "backend/src/plugins/translate/_text_cleaner.py": 13161219043687449729, + "venv/lib/python3.13/site-packages/pygments/lexers/arrow.py": 6699009974224818979, + "frontend/src/lib/stores/__tests__/mocks/navigation.js": 16028014574825445651, + "venv/lib/python3.13/site-packages/pandas/_libs/arrays.cpython-313-x86_64-linux-gnu.so": 8634234308863090562, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_dtypes.py": 2313400030798822026, + "docs/api/html/group__AuthRepository.html": 8945062834514291149, + "docs/api/html/group____process__dashboards__for__start.html": 12366425439832314749, + "venv/lib/python3.13/site-packages/httpcore/_synchronization.py": 10997080307461682786, + "docs/api/html/group__test__get__users__page__preserves__primary__schema__error__over__fallback__auth__error.html": 92546336677002310, + "backend/src/plugins/translate/dictionary_entries.py": 11394786590056973146, + "docs/api/html/group__test__create__api__key.html": 9726291706125777558, + "docs/api/html/group__test__database__url__env__override.html": 11621545109542978109, + "venv/lib/python3.13/site-packages/PIL/ImageMode.py": 2361901066832921091, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_pydatetime.py": 16228315407662560992, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py": 10917162988625838981, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mariadb.py": 3109042476851791996, + "backend/src/core/task_manager/graph.py": 3769962371015813191, + "docs/api/html/group__ValidationFinding.html": 10426135687421865521, + "venv/lib/python3.13/site-packages/keyring/__main__.py": 14602212239605295451, + "venv/lib/python3.13/site-packages/pygments/lexers/resource.py": 8057351545636349791, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numerictypes.py": 8836108334624807647, + "venv/lib/python3.13/site-packages/numpy/core/arrayprint.py": 9652368112174245943, + "docs/api/html/group__test__get__dashboards__success.html": 1205024512730425920, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_categorical.py": 8844412630511751140, + "docs/api/html/group__update__git__config.html": 5484250484604366906, + "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.pyi": 3111521453255595221, + "venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py": 10265081145401348863, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_defmatrix.py": 2982572083770917411, + "docs/api/html/navtreeindex8.js": 16014422725768627041, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/min_max_.py": 13457611930368041821, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_comparison.py": 5348781442514871089, + "venv/lib/python3.13/site-packages/sqlalchemy/event/__init__.py": 16182540084654095394, + "venv/bin/uvicorn": 3776670267111625708, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____getSessionUiPhase.html": 8539042222924950819, + "docs/api/html/group____compute__source__hash.html": 7685714365612338715, + "backend/src/api/routes/git/_router.py": 14821560101452369211, + "docs/api/html/group__login__for__access__token.html": 5634643443334465635, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setError.html": 16852758973526195279, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/sum_.py": 7062070517544958546, + "docs/api/html/group__ClarificationSessionDto.html": 15558163197172650048, + "docs/api/html/group__SupersetClientGetChart.html": 12922958291009873472, + "venv/lib/python3.13/site-packages/pydantic_core/py.typed": 15130871412783076140, + "docs/api/html/group__get__trace__id.html": 8371547524761107402, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_federal.py": 16758212916399857971, + "docs/api/html/group__test__cot__json__formatter__output.html": 8496099755020894677, + "docs/api/html/group__test__manifest__versioning__and__immutability.html": 7762946147479843686, + "venv/lib/python3.13/site-packages/referencing/tests/test_retrieval.py": 10782087416909177703, + "docs/api/html/group__setup__class.html": 16392913197681512262, + "docs/api/html/group__SupersetCompilationAdapter__create__sql__lab__session.html": 13996430015600704960, + "docs/api/html/group__handleDeleteRole__Function.html": 12113537236786108342, + "venv/lib/python3.13/site-packages/pydantic/type_adapter.py": 10162988530225478692, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_public_dtype_api_table.h": 8933016749764021181, + "venv/lib/python3.13/site-packages/jeepney/auth.py": 10565040329507087892, + "docs/api/html/group____generate__unique__name.html": 17753267115013703674, + "docs/api/html/group__call__llm__for__batch.html": 16443633855674435189, + "docs/api/html/group__ReportsService.html": 2032865134635125460, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/gh_22819.pyf": 14717748358749934995, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex.py": 16348619731902304844, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/bulk_persistence.py": 2307818727114646866, + "docs/api/html/group__loadHistory__Function.html": 688973918186380673, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/__init__.py": 4337679908168515343, + "backend/src/services/clean_release/stages/base.py": 14966559035592717091, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/LICENSE": 6989336036499641077, + "backend/src/core/logger.py": 8723767731682363522, + "venv/lib/python3.13/site-packages/authlib/oauth2/client.py": 7237359250450222680, + "docs/api/html/group__GitServiceBase.html": 3176284003306222212, + "venv/lib/python3.13/site-packages/numpy/ma/extras.pyi": 15292036493781556342, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_datetime.py": 13781712980921881470, + "venv/lib/python3.13/site-packages/pygments/lexers/teraterm.py": 16132676290138105984, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/automap.py": 4672057253309608464, + "venv/lib/python3.13/site-packages/packaging/_musllinux.py": 2568338440457080365, + "frontend/src/lib/components/translate/TranslationRunGlobalIndicator.svelte": 13452471817010485891, + "backend/git_repos/remote/test-repo.git/objects/cf/d2dcbd55735b8cedf21e04e64ac627c6f9a458": 10982669277486885525, + "backend/src/services/clean_release/policy_engine.py": 7078053766918689048, + "docs/api/html/group__test__parse__superset__link__dashboard__slug__recovery.html": 11791096107070839073, + "venv/lib/python3.13/site-packages/numpy/lib/_npyio_impl.pyi": 6576645822227068922, + "backend/src/plugins/translate/orchestrator_planner.py": 14664171612247380324, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pluggy/_manager.py": 18202550145040079612, + "merge_spec.py": 13855568343782022, + "docs/api/html/group__DashboardHub__getSortValue__Function.html": 10869826115392569762, + "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/LICENSE": 16510279186416777136, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_string.py": 9224234539232195189, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.cpython-313-x86_64-linux-gnu.so": 12473903676666680134, + "venv/lib/python3.13/site-packages/pydantic/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpython-313-x86_64-linux-gnu.so": 7659849621107414188, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_fileno.py": 15541504502174047167, + "venv/lib/python3.13/site-packages/pandas/tests/apply/common.py": 1113935558495455492, + "venv/lib/python3.13/site-packages/requests/models.py": 9104898985870652256, + "venv/lib/python3.13/site-packages/pandas/_libs/join.cpython-313-x86_64-linux-gnu.so": 2947947484481830203, + "venv/lib/python3.13/site-packages/pandas/plotting/_misc.py": 10202078355301731720, + "venv/lib/python3.13/site-packages/pandas/tests/strings/test_extract.py": 14547819021277949280, + "venv/lib/python3.13/site-packages/anyio/functools.py": 4300781896135050327, + "docs/api/html/navtreeindex13.js": 13124871548171693035, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_reduction.py": 2671872764983553906, + "docs/api/html/group__initializeForm__Function.html": 9614464266538572602, + "venv/lib/python3.13/site-packages/anyio/__init__.py": 12752386096259972231, + "venv/lib/python3.13/site-packages/attr/_version_info.py": 3850449653182103457, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/socks.py": 17581293673028078134, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py": 14576109242055156193, + "venv/lib/python3.13/site-packages/pip/_vendor/__init__.py": 11734104142988812647, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/exceptions.py": 2873060848310644552, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/aws.py": 1364297790170663941, + "venv/lib/python3.13/site-packages/urllib3/util/ssl_.py": 11720964010284185952, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/aead.py": 8740476942925346444, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/np_datetime.pyi": 2884703536776830012, + "docs/api/html/group__test__confirmation__owner__mismatch__returns__403.html": 2005624954638348501, + "frontend/build/_app/immutable/chunks/CjSfQumm.js": 13507568424833951161, + "backend/src/core/auth/logger.py": 11543018423739293926, + "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/greenlet/greenlet_refs.hpp": 8525862687669040510, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/warnings_and_errors.py": 11172336980866895356, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo90.f90": 58572216018544419, + "docs/api/html/group__DatasetItem.html": 14862648460163668817, + "docs/api/html/group__test__task__manager.html": 16353988531673977488, + "docs/api/html/group__StatsCounts.html": 11606389860887165837, + "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/WHEEL": 2357997949040430835, + "venv/lib/python3.13/site-packages/pygments/lexers/ptx.py": 4855144232064607995, + "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.pyi": 1424133074692438241, + "docs/api/html/search/search.js": 11014809740432474012, + "docs/api/html/dir_49e56c817e5e54854c35e136979f97ca.js": 3785536190143816087, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_crackfortran.py": 10278720705326601943, + "backend/src/plugins/backup.py": 1465877198420371496, + "backend/src/plugins/llm_analysis/__init__.py": 9254812131558249924, + "venv/lib/python3.13/site-packages/fastapi/security/open_id_connect_url.py": 3502444317759870079, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/dtype_api.h": 17040779421165903019, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.pyi": 16372699564794732463, + "venv/lib/python3.13/site-packages/websockets/legacy/http.py": 16266924390281399720, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/cursor.py": 8074527784886981710, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh17797.f90": 17319074662077763768, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/twisted.py": 16059862619165885730, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_finfo.py": 17174674209412768422, + "frontend/src/lib/components/translate/ScheduleConfig.svelte": 9548866160759602637, + "backend/tests/test_translate_history.py": 12742653877430920309, + "venv/lib/python3.13/site-packages/pandas/io/html.py": 2727247063231114153, + "frontend/build/_app/immutable/chunks/Cm9ytnzq.js": 10252107288512057461, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_constructors.py": 6727209833819502644, + "docs/api/html/group__DictionaryResolutionResult.html": 4103555385968464679, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/boxplot.py": 7090586111222952862, + "venv/lib/python3.13/site-packages/pydantic/types.py": 785880062468926292, + "frontend/src/lib/utils/debounce.js": 11611676161433422398, + "scripts/gen_semantics.py": 11397714264377363083, + "venv/lib/python3.13/site-packages/pandas/tests/computation/test_eval.py": 2921337784087808487, + "venv/lib/python3.13/site-packages/numpy/core/_dtype_ctypes.py": 17055498803745289016, + "docs/api/html/group__DashboardHub__getValidationLabel__Function.html": 15200568549441097956, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/azure.py": 13529763490186254462, + "venv/lib/python3.13/site-packages/numpy/fft/__init__.pyi": 10354990065626209946, + "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/METADATA": 3814104793342833872, + "docs/api/html/group__calculate__schedule.html": 15388949849298396012, + "venv/lib/python3.13/site-packages/pillow.libs/libxcb-64009ff3.so.1.1.0": 7322566017497612912, + "docs/api/html/group__GitDashboardPage.html": 4862973845020805849, + "venv/lib/python3.13/site-packages/pygments/lexers/esoteric.py": 16839499623020685146, + "docs/api/html/group__clearTasks__Function.html": 13599249766000880879, + "venv/lib/python3.13/site-packages/fastapi/__main__.py": 10755252341326986079, + "docs/api/html/group__ReportsListTest__Module.html": 16813306558696724690, + "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.py": 1809069379864985298, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.py": 1341849861184131135, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/REQUESTED": 15130871412783076140, + "docs/api/html/group__believed.html": 3317841457835430076, + "venv/lib/python3.13/site-packages/pip/_internal/commands/check.py": 6937252393012787214, + "venv/lib/python3.13/site-packages/pip/_internal/utils/logging.py": 9639351759822608724, + "venv/lib/python3.13/site-packages/pygments/lexers/sgf.py": 6802546617450210439, + "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/RECORD": 14375680579341422646, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/__init__.py": 15130871412783076140, + "docs/api/html/group____match__dataset__review__field.html": 2398745550798139744, + "backend/src/api/routes/assistant/_dataset_review_dispatch.py": 8488841336355543230, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/RECORD": 17341460504914308184, + "docs/api/html/group____resolve__env__id.html": 6378272965191199149, + "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/__init__.py": 10920760520224795596, + "venv/lib/python3.13/site-packages/referencing/_attrs.py": 14434891919747863272, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_regression.py": 16266600037241221681, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_integrity.py": 12010564145080323987, + "venv/lib/python3.13/site-packages/numpy/fft/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_pickle.py": 2910383574326146698, + "docs/api/html/group__handleDeleteGiteaRepo__Function.html": 8343489638685254730, + "docs/api/html/menudata.js": 1163638699645288191, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_data_list.py": 1131520422778420758, + "frontend/build/_app/immutable/chunks/DOLPDwzw.js": 4329576946217321861, + "docs/api/html/group__SupersetContextTemplatesMixin____append__template__variable.html": 14259073888034384633, + "docs/api/html/group__get__user__by__id.html": 7396712021803572982, + "venv/lib/python3.13/site-packages/click/parser.py": 13988544851350599869, + "venv/lib/python3.13/site-packages/pandas/core/internals/concat.py": 12151675567885981464, + "docs/api/html/group__ValidationResult.html": 15773352481010235482, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_year.py": 17441853503700184299, + "frontend/src/lib/components/dataset-review/__tests__/validation_findings_panel.ux.test.js": 16008187945694980240, + "venv/lib/python3.13/site-packages/websockets/legacy/auth.py": 16685128086396181286, + "docs/api/html/group__validate__target__table__schema.html": 17860602982230273522, + "venv/lib/python3.13/site-packages/pandas/tests/apply/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/stride_tricks.pyi": 8257977532799807847, + "venv/lib/python3.13/site-packages/pandas/_libs/groupby.pyi": 6154952803757623626, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_join.py": 1697622393195784944, + "backend/src/services/validation_service.py": 16912151200133637828, + "venv/lib/python3.13/site-packages/ecdsa/test_keys.py": 17117188373419689467, + "venv/lib/python3.13/site-packages/pycparser/c_lexer.py": 10964496587169353564, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_constructors.pyi": 164538008396001632, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/licenses/LICENSE": 5757922580822401219, + "docs/api/html/group____ensure__git__server__configs__columns.html": 11324045515305585, + "venv/lib/python3.13/site-packages/jose/__init__.py": 15575670403380555945, + "venv/lib/python3.13/site-packages/PIL/PsdImagePlugin.py": 4347581352716313298, + "venv/lib/python3.13/site-packages/pygments/styles/bw.py": 869630132791421268, + "backend/tests/test_logger.py": 10218144331624268519, + "docs/api/html/group__MaintenanceDashboardState.html": 12984094897536307625, + "docs/api/html/group__ClarificationSession.html": 5830007380019646698, + "docs/api/html/group__save__preview.html": 10710352658491088679, + "docs/api/html/group__DatasetReviewEntryUxTests.html": 11847231912591045967, + "venv/lib/python3.13/site-packages/authlib/integrations/django_client/__init__.py": 14186136918454802843, + "docs/api/html/group____log.html": 10162958821169656028, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/abstract_interface/foo.f90": 14491312943942432024, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_rowcount.py": 6682800584070485197, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_compound.f90": 15012692484070385863, + "docs/api/html/group__GitLLMExtension.html": 13438682288527399729, + "venv/lib/python3.13/site-packages/urllib3/util/__init__.py": 2848870790459558599, + "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.pyi": 10360538024015064036, + "docs/api/html/group__report__repository.html": 57313121527708932, + "docs/api/html/group__TranslateHelpersModule.html": 2380085236737921146, + "docs/api/html/group__DemoDataService.html": 12469614615958816935, + "venv/lib/python3.13/site-packages/annotated_doc/__init__.py": 6834198874252778425, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_index.py": 14187428934500390688, + "venv/lib/python3.13/site-packages/charset_normalizer/__init__.py": 12412600648990392633, + "docs/api/html/sync_off.png": 12901520360373226022, + "docs/api/html/group__ApiModule.html": 8246096891534355769, + "venv/lib/python3.13/site-packages/jaraco/classes/meta.py": 15930255620248873425, + "docs/api/html/group____get__repository.html": 857041345230199829, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/fields.py": 10090313990429695520, + "docs/api/html/group__FindingSeverity.html": 10603170265602472688, + "docs/api/html/group__update__preferences.html": 18008584156403722342, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/__init__.py": 14571251344510763303, + "docs/api/html/group__test__get__run__detail__not__found.html": 9435086798292174243, + "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.py": 15854257948148516490, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/path_registry.py": 8410829659513707067, + "venv/lib/python3.13/site-packages/h11/__init__.py": 222801976520197308, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_arrayterator.py": 1646530870049791291, + "venv/lib/python3.13/site-packages/pygments/lexers/crystal.py": 3579547906957125755, + "venv/lib/python3.13/site-packages/httpcore/_sync/http2.py": 11167798787131917524, + "venv/lib/python3.13/site-packages/pip/_internal/models/search_scope.py": 9282393652860589543, + "docs/api/html/group__get__clean__release__repository.html": 3802772279404902522, + "docs/api/html/group__detect__jinja__spans.html": 15712299430254929498, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py": 10196543528439925412, + "venv/lib/python3.13/site-packages/passlib/pwd.py": 14727089240749133060, + "docs/api/html/group__TranslateSchedulesApi.html": 2216535141847218372, + "docs/api/html/group__test__case__insensitive__matching.html": 9410338803903481371, + "docs/api/html/group__test__persist__task__with__datetime__in__params.html": 11036090690820046897, + "docs/api/html/globals.html": 8891371690034474264, + "venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py": 13742159245132241245, + "venv/lib/python3.13/site-packages/pygments/styles/sas.py": 12363791134941577382, + "backend/src/core/utils/dataset_mapper.py": 3419786058088183802, + "docs/api/html/group__test__end__active__event__single__banner.html": 329198841271298860, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_timedelta.py": 12822713918114715561, + "venv/lib/python3.13/site-packages/pygments/lexers/templates.py": 16267123844830647955, + "docs/api/html/search/groups_14.js": 15467890647915855647, + "venv/lib/python3.13/site-packages/cffi/cffi_opcode.py": 8411344362428947268, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_function_base.pyi": 2792940598392356268, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/provision.py": 13156893203444936309, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_common.h": 14732581790650403546, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/_cmd.py": 14954780419054588164, + "docs/api/html/group__test__table__based__match.html": 4423679603659278105, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/comparisons.py": 7327629888601690759, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arrow_interface.py": 5231449831607036031, + "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.py": 338595621218646492, + "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.pyi": 1942236362568669510, + "docs/api/html/group__buildParams__Function.html": 18347180877148300889, + "venv/lib/python3.13/site-packages/numpy/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_dtypes.py": 6831042272921908068, + "venv/lib/python3.13/site-packages/pandas/_testing/__init__.py": 10127496080404419481, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_isin.py": 8136794647591341784, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/__init__.py": 15130871412783076140, + "docs/api/html/group__test__belief__scope__reason__not__visible__at__info.html": 4840136292915497368, + "venv/lib/python3.13/site-packages/urllib3/util/wait.py": 14629837381061032546, + "docs/api/html/group__GitServiceContractTests.html": 220168520751414086, + "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/zip-safe": 15240312484046875203, + "dist/docker/ss-tools-frontend.0.1.5.tar.xz": 4350594811664018944, + "frontend/build/_app/immutable/nodes/22.DcvXseLS.js": 4398688763230681693, + "backend/src/services/maintenance/_chart_manager.py": 17709049211413698908, + "docs/api/html/group__test__get__clarification__state__returns__empty__payload__when__session__has__no__record.html": 13354220940558115259, + "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.pyi": 3661113433078225952, + "docs/api/html/group____valid__artifacts.html": 18020364086775197515, + "docs/api/html/group__frontend__src__lib__auth__store__ts____createAuthStore.html": 4553975624933631744, + "venv/lib/python3.13/site-packages/pygments/lexers/elpi.py": 5736689911777977822, + "docs/api/html/group__ApiKeyCreateRequest.html": 14624348983729327471, + "docs/api/html/group__test__patch__profile__preferences__success.html": 3112680572132768752, + "backend/src/plugins/translate/_batch_proc.py": 16308423934579092314, + "backend/git_repos/remote/test-repo.git/objects/ee/59553241ef084b83d3351ba4c33caf2ed9a840": 11587106407031972706, + "docs/api/html/group__test__combined__metrics__live__and__snapshot.html": 3514785552121043708, + "docs/api/html/group__test__configure__logger__task__log__level.html": 523141612683121323, + "docs/api/html/group__LaunchStatus.html": 3552441502967473600, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/WHEEL": 3749476255729626245, + "docs/api/html/group__oauth2__scheme.html": 13377767828964490913, + "docs/api/html/globals_r.html": 7591223539833121046, + "docs/api/html/group__services.html": 715009395158677164, + "docs/api/html/group__AuditService.html": 14546698565795749486, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/METADATA": 13776251749114562660, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix.py": 4440659352851541547, + "venv/lib/python3.13/site-packages/h11/py.typed": 12358286306858197118, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/WHEEL": 12054782055241301457, + "frontend/src/lib/components/ui/SearchableMultiSelect.svelte": 1872229619122554196, + "docs/api/html/group__navigateUp__Function.html": 14835818415678519228, + "docs/api/html/group__test__pagination__boundaries.html": 7157080918011904103, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/__init__.py": 7316739139609490520, + "docs/api/html/group__update__logging__config.html": 18321262860475642376, + "docs/api/html/group__TranslationScheduleItem.html": 12108491525286964033, + "venv/lib/python3.13/site-packages/keyring/compat/__init__.py": 7578020229796085753, + "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/tokens_mixins.py": 9923721909178724437, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api1.c": 8494977275215578998, + "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/licenses/LICENSE": 1126906383149772681, + "docs/api/html/group__AdaptiveBatchSizer.html": 3807050557665810980, + "venv/lib/python3.13/site-packages/pygments/lexers/soong.py": 10273453675034306814, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_size.py": 14032390806503440750, + "backend/tests/core/test_mapping_service.py": 893971694075390976, + "docs/api/html/group__MigrationPlugin.html": 12305721336063597680, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/missing.py": 14589973874359353799, + "venv/lib/python3.13/site-packages/pandas/tests/strings/conftest.py": 12065119814157197510, + "frontend/playwright-report/trace/snapshot.v8KI4P3m.js": 7854911091722310751, + "venv/lib/python3.13/site-packages/greenlet/greenlet_compiler_compat.hpp": 465436185632243991, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufuncs.pyi": 1808147181379894521, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/RECORD": 1164466882742493155, + "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/checks.pyx": 13187505236986855177, + "venv/lib/python3.13/site-packages/pandas/tests/window/__init__.py": 15130871412783076140, + "docs/api/html/group__get__providers.html": 2676511877365075835, + "docs/api/html/group__frontend__src__lib__stores__translationRun__js____stopTranslationRun.html": 2429083995964044538, + "venv/lib/python3.13/site-packages/anyio/streams/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/idna/core.py": 7725813731640753267, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_ufunc.py": 669019702416198889, + "backend/src/core/task_manager/persistence.py": 7819185702810966930, + "venv/lib/python3.13/site-packages/_pytest/cacheprovider.py": 5114545353355728201, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/version.py": 5000350193835202949, + "docs/api/html/group__delete__environment.html": 14176437049644341788, + "venv/lib/python3.13/site-packages/pygments/lexers/lean.py": 14518351619544450286, + "backend/src/api/routes/maintenance/_routes.py": 15905969964117673493, + "docs/api/html/group__CHARS__PER__TOKEN__MIXED.html": 1704290881184281632, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/scalar_string.f90": 15018631706178918578, + "docs/api/html/group__test__get__dashboards__profile__filter__matches__display__alias__without__detail__fanout.html": 15114828617044175570, + "venv/lib/python3.13/site-packages/pydantic/v1/typing.py": 13467331592583800299, + "frontend/build/_app/immutable/chunks/C_135Pqo.js": 15633649195241643441, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_astype.py": 8505605634958454768, + "venv/lib/python3.13/site-packages/jeepney/tests/secrets_introspect.xml": 10717038359151125278, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/live_render.py": 14619617939683800204, + "venv/lib/python3.13/site-packages/fastapi/routing.py": 466387159689567541, + "backend/git_repos/remote/test-repo.git/hooks/commit-msg.sample": 13435789416588783681, + "docs/api/html/group__error.html": 3228457887146363212, + "venv/lib/python3.13/site-packages/numpy/ma/README.rst": 13203411621071845216, + "docs/api/html/group____record__session__event.html": 3602405687107437058, + "docs/api/html/group__SessionCollaboratorDto.html": 13064735570529712848, + "venv/lib/python3.13/site-packages/PIL/_imagingmorph.pyi": 18222325750818585549, + "venv/lib/python3.13/site-packages/pydantic/_internal/_fields.py": 2692892815545218335, + "docs/api/html/group__test__transform__yaml__ignores__unmapped__uuid.html": 2406741085212843540, + "docs/api/html/group__test__manifest__build__contract.html": 6734412557509934205, + "venv/lib/python3.13/site-packages/h11/_writers.py": 8585823094975171480, + "docs/api/html/group__test__create__gitea__pull__request__returns__branch__error__when__target__missing.html": 12846340033666855401, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.pyi": 6616985736007157309, + "venv/lib/python3.13/site-packages/iniconfig/_version.py": 8497474654470220170, + "docs/api/html/group__readJson__Function.html": 17492082868349391161, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/flatiter.pyi": 9598576095384661517, + "frontend/src/lib/stores/maintenance.svelte.js": 17672315346665850067, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_compat.py": 11481259393140789881, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation": 4826455560768665215, + "docs/api/html/group____FakeSession.html": 16348083183205532802, + "docs/api/html/group__StoresModule.html": 154732489318359356, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/attributes.py": 5068814626384132900, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_rename_axis.py": 12432900682994622455, + "venv/lib/python3.13/site-packages/pandas/_testing/asserters.py": 272063856832951430, + "docs/api/html/group__teardown__class.html": 7748593185053531837, + "docs/api/html/group__CompiledPreview.html": 13362113672066090539, + "docs/api/html/group__SupersetClientBase.html": 11179460536607971200, + "venv/lib/python3.13/site-packages/rsa/cli.py": 1753949198467694190, + "backend/src/services/dataset_review/orchestrator.py": 10195260399518167323, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_log_render.py": 18076931145915444473, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_logical_ops.py": 4438104524064299182, + "docs/api/html/group__frontend__src__services__taskService__js____resolveTask.html": 1005823194380786515, + "docs/api/html/group__list__execution__mappings.html": 5449792529029019677, + "docs/api/html/group__StartSessionCommand.html": 15495582448553758086, + "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__SupersetClientGetDashboardsPage.html": 14973966407203177325, + "venv/lib/python3.13/site-packages/pandas/_libs/ops.cpython-313-x86_64-linux-gnu.so": 10490009128553397250, + "docs/api/html/search/functions_d.js": 5419224988785173409, + "venv/lib/python3.13/site-packages/ecdsa/util.py": 10543788809724099209, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_cumulative.py": 16283954014170928273, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufuncs.pyi": 14995898537484305307, + "venv/lib/python3.13/site-packages/numpy/random/__init__.pyi": 13833571330152615376, + "docs/api/html/group__belief__scope.html": 2116670201736502743, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/discovery.py": 12479996594187326798, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/coercions.py": 5112358449376598662, + "backend/git_repos/remote/test-repo.git/objects/f7/6ee984d9d47c3cd0b362fdf60a47162bbc202d": 5351983130474596028, + "venv/lib/python3.13/site-packages/PIL/JpegPresets.py": 12342262642420923471, + "frontend/src/components/Toast.svelte": 10476309094915986605, + "docs/api/html/group__UserDashboardPreference.html": 6859297743426015504, + "docs/api/html/group__DictionaryEntry.html": 16163430495584463690, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/zip-safe": 15240312484046875203, + "venv/lib/python3.13/site-packages/pip/_internal/cli/index_command.py": 15798187258841839292, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/__init__.py": 15130871412783076140, + "backend/tests/core/migration/test_dry_run_orchestrator.py": 5297580787144927622, + "venv/lib/python3.13/site-packages/psycopg2/extras.py": 7510107741729866600, + "docs/api/html/group__test__resolve__merge__conflicts__passes__resolution__items__to__service.html": 3503298276532245309, + "docs/api/html/group__frontend__src__lib__stores__health__js____createHealthStore.html": 18071199202062130010, + "docs/api/html/group__enable__schedule.html": 10104484743481845781, + "docs/api/html/group__CleanProfilePolicy.html": 16264819993053610464, + "docs/api/html/search/groups_6.js": 13269109705568320590, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawerForTaskIfPreferred.html": 7335878052514362026, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/response.py": 353446217435156470, + "docs/api/html/group__test__create__user.html": 11963320393863284451, + "venv/lib/python3.13/site-packages/rsa/pem.py": 3118524021576082054, + "venv/lib/python3.13/site-packages/passlib/tests/tox_support.py": 9200774792427691602, + "venv/lib/python3.13/site-packages/pandas/core/arrays/numpy_.py": 16641729409992576358, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_argon2.py": 11345706437113628133, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/result.py": 8306497889916212025, + "backend/tests/services/clean_release/test_compliance_execution_service.py": 13313999445101550820, + "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/METADATA": 3390257212824996989, + "docs/api/html/group____resolve__profile__actor__aliases.html": 11823740193965915284, + "venv/lib/python3.13/site-packages/pip/_internal/utils/_log.py": 13011536737418204268, + "venv/lib/python3.13/site-packages/numpy/ma/__init__.pyi": 1855615156913010107, + "docs/api/html/group__AsyncAPIClient____build__api__url.html": 2646689845586170671, + "docs/api/html/group__SettingsPageUxTest__Module.html": 2448306819139198817, + "venv/lib/python3.13/site-packages/h11/_version.py": 9334144031400715479, + "frontend/src/routes/settings/__tests__/settings_page.integration.test.js": 4898515600493251972, + "backend/src/core/superset_client/_charts.py": 15609724925814918066, + "frontend/src/lib/components/reports/__tests__/report_type_profiles.test.js": 8052627851850386760, + "docs/api/html/group____estimate__markdown__height.html": 16586606759606629897, + "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so": 13917011878259810696, + "docs/api/html/group__detect__language.html": 18333706809382540759, + "docs/api/html/group__CotJsonFormatter.html": 14965099602834571127, + "venv/lib/python3.13/site-packages/python_multipart/decoders.py": 14252672585219598429, + "frontend/src/lib/components/dataset-review/ExecutionMappingReview.svelte": 17593699210431624897, + "docs/api/html/group__EffectiveProfileFilter.html": 17635576406887586988, + "venv/lib/python3.13/site-packages/pip/_internal/commands/inspect.py": 1401259241736437656, + "frontend/playwright-report/trace/manifest.webmanifest": 565917384317025203, + "venv/lib/python3.13/site-packages/_pytest/pytester_assertions.py": 1899505062406979663, + "docs/api/html/group__MigrationSettings.html": 7191961132337035743, + "venv/lib/python3.13/site-packages/anyio/lowlevel.py": 11919467357746648592, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_libfrequencies.py": 16302676625743291594, + "docs/api/html/group__delete__task.html": 11691883776137464961, + "venv/lib/python3.13/site-packages/psycopg2/errorcodes.py": 6711830404954314463, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_datetime64.py": 16272266806084058743, + "backend/src/api/routes/settings.py": 3087014377952845524, + "docs/api/html/group__currentPage__Data.html": 5033996641622029546, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token.py": 4747545058753492487, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.py": 17993169044119596860, + "venv/lib/python3.13/site-packages/_pytest/_io/terminalwriter.py": 16852767249156780980, + "backend/src/core/superset_client/__init__.py": 3185082867840025180, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_produces_warning.py": 750781117570375777, + "docs/api/html/group__test__already__processed__event.html": 11093851268510393622, + "venv/lib/python3.13/site-packages/cryptography/__about__.py": 10471312538528457144, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/pubprivmod.f90": 8680230823849248800, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_handlers.py": 556092537518149789, + "venv/lib/python3.13/site-packages/pydantic/warnings.py": 5142352784799639092, + "docs/api/html/group__User.html": 7890454832671400915, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____updateSettings.html": 4714555805820334963, + "venv/lib/python3.13/site-packages/passlib/handlers/argon2.py": 835074952952017434, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/__init__.py": 15130871412783076140, + "docs/api/html/group__CleanReleaseConfig.html": 5411302572007077971, + "docs/api/html/tab_a.png": 119030213786483377, + "venv/lib/python3.13/site-packages/pydantic/v1/_hypothesis_plugin.py": 6383299780629629080, + "venv/lib/python3.13/site-packages/pandas/io/excel/_odswriter.py": 17220892094904423676, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_indexing.py": 14552860427542050771, + "venv/lib/python3.13/site-packages/uvicorn/_compat.py": 2593000889619875759, + "venv/lib/python3.13/site-packages/packaging/_parser.py": 13394093358675095309, + "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py": 9627328185578406121, + "venv/lib/python3.13/site-packages/pytest/__main__.py": 8747175113746141224, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_common.py": 12064185052519470358, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py": 9673372914396625461, + "docs/api/html/group__SuggestRequest.html": 4340073923627780117, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE.PSF": 13208428090829817329, + "venv/lib/python3.13/site-packages/fastapi/responses.py": 6092572095549573593, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/api.py": 12339139862411341381, + "venv/lib/python3.13/site-packages/pandas/_libs/join.pyi": 369262334342439364, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group____ensure__auth__users__columns.html": 3871758539262511602, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth2_client.py": 6742098857947604729, + "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/licenses/LICENSE": 10253862887434903467, + "venv/lib/python3.13/site-packages/numpy/lib/_datasource.pyi": 5894423389796456573, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/RECORD": 1051307407005554409, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py": 6509914623926583629, + "venv/lib/python3.13/site-packages/PIL/ImageFile.py": 15765130658900167517, + "backend/src/core/utils/superset_compilation_adapter.py": 7324857004677252529, + "docs/api/html/globals_b.html": 2050803601864762326, + "frontend/playwright-report/trace/snapshot.html": 11716473056415565262, + "backend/tests/test_translate_executor_filter.py": 10523604039625597391, + "venv/lib/python3.13/site-packages/PIL/_tkinter_finder.py": 14045496501810535432, + "docs/api/html/group__submit__correction.html": 14575546737554094961, + "docs/api/html/group__GitPluginModule.html": 9711439707382347339, + "docs/api/html/group__test__trigger__run__no__dashboards.html": 16608562786085903819, + "venv/lib/python3.13/site-packages/idna/idnadata.py": 200840860629525970, + "venv/lib/python3.13/site-packages/pydantic/v1/types.py": 10761779263335288559, + "backend/tests/test_deprecation_audit_fixes.py": 970343857087924255, + "docs/api/html/group____register__plugin.html": 4747901438274521925, + "docs/api/html/group__test__sync__environment__handles__api__error__gracefully.html": 3531439474218526807, + "backend/git_repos/remote/test-repo.git/hooks/push-to-checkout.sample": 4825338927654285910, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/__init__.py": 8807410829260170597, + "docs/api/html/group__test__list__events.html": 8621900407061858699, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/url.py": 1558176735588971829, + "docs/api/html/group__ProfilePreferencesIntegrationTest.html": 5649513853335976994, + "dist/docker/ss-tools-frontend.0.1.4.tar.xz": 14537310367203878814, + "venv/lib/python3.13/site-packages/pygments/lexers/func.py": 12257919753150082714, + "backend/src/core/__tests__/test_superset_preview_pipeline.py": 13996819877413389729, + "docs/api/html/group__list__ad__mappings.html": 4036472726252673441, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_repr.py": 11737029058271003097, + "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/backend.py": 9072112660082423691, + "docs/api/html/globals_func_i.html": 8474102779220933859, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py": 8412594868333244844, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/shape.pyi": 2188930129299348535, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_c_parser_only.py": 3038797485572319804, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_docs.py": 17898499890274685198, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_selectable_constructors.py": 84919308502240880, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/RECORD": 7810411361439269754, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/licenses/LICENSE": 12684353321716745791, + "backend/src/core/superset_client/_datasets.py": 10922216252382553392, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_sequence.py": 3721630758772772345, + "venv/lib/python3.13/site-packages/anyio/_core/_sockets.py": 10525237827959090532, + "backend/src/core/__tests__/test_native_filters.py": 3247249287628984886, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/reflection.py": 5951113078754792806, + "docs/api/html/search/all_18.js": 11356236066410231589, + "docs/api/html/group__SupersetClientAuthenticate.html": 14826114222268403337, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/flatiter.pyi": 6885824496927786205, + "docs/api/html/group__TASKS__DATABASE__URL.html": 7804801392493995117, + "backend/src/api/routes/health.py": 13097980848666987664, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/__init__.py": 10834852742917144496, + "docs/api/html/group__LLMHttpClient.html": 14802937964955012182, + "docs/api/html/group__AsyncAPIClient__get__headers.html": 4827871173278773347, + "docs/api/html/globals_func_e.html": 5853577764297783560, + "docs/api/html/globals_func.js": 13107479563907806427, + "venv/lib/python3.13/site-packages/httpx/_decoders.py": 14478643473006178492, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_arithmetic.py": 17449383588949771785, + "docs/api/html/group__MapperPlugin.html": 5002883678765248833, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/modules.py": 9023244176010175562, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_assignability.pyi": 1718610984396277997, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_factorize.py": 861897342553280506, + "venv/lib/python3.13/site-packages/git/types.py": 13960696584646279283, + "venv/lib/python3.13/site-packages/urllib3/util/ssltransport.py": 1175688460659745788, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_interval.py": 2911379732293827726, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/models.py": 17356033861792227817, + "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.cpython-313-x86_64-linux-gnu.so": 16327106317503267067, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setLoading.html": 8035127119828589627, + "docs/api/html/group__test__key__table__sorting.html": 7072979890307169502, + "venv/lib/python3.13/site-packages/starlette/endpoints.py": 3379755567024530053, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py": 13454394827268856575, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py": 17873236864291624476, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/__init__.py": 14571251344510763303, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_api_info.pyi": 7887489694871300469, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarprint.py": 9482100952562512933, + "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.py": 1262543237476191714, + "venv/lib/python3.13/site-packages/pandas/core/methods/describe.py": 17006836623161521092, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_s3.py": 17425535047477778484, + "backend/src/api/routes/dashboards/__init__.py": 14480706693953038818, + "backend/src/services/dataset_review/orchestrator_pkg/_commands.py": 2554792474786179775, + "backend/tests/test_api_key_routes.py": 12985233160643853260, + "venv/lib/python3.13/site-packages/anyio/_core/_signals.py": 1204322949639513552, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ext.py": 8361831054662096695, + "docs/api/html/group____extract__columns__from__rows.html": 6576741443241931816, + "docs/api/html/group__sidebar__Store.html": 2096791117586109991, + "docs/api/html/group____accumulate__objects.html": 12340985983036661997, + "docs/api/html/group__ReportBuilder.html": 16300601000239313366, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/key_set.py": 8485970749081951741, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/aioodbc.py": 12704729841432715246, + "frontend/src/routes/storage/backups/+page.svelte": 9728305121470326167, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/iniconfig/_parse.py": 11418441008427032090, + "frontend/src/routes/translate/dictionaries/+page.svelte": 1038433841421198720, + "docs/api/html/group__ValidationFindingDto.html": 9426027159119514734, + "docs/api/html/group__delete__user.html": 17189025779640639203, + "docs/api/html/group__test__get__datasets__search.html": 7665831581777860619, + "docs/api/html/group__deleteAssistantConversation__Function.html": 8387243433121457609, + "docs/api/html/group__SupersetContextExtractorBase____extract__numeric__identifier.html": 8466073084601965618, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/urllib3/connection.py": 12761371748350595804, + "venv/bin/Activate.ps1": 1196103427939049710, + "venv/lib/python3.13/site-packages/pip/_internal/models/link.py": 7172346414535657925, + "docs/api/html/group__get__metrics.html": 15776096111007526779, + "venv/lib/python3.13/site-packages/pygments/lexers/hdl.py": 6772823608124143123, + "docs/api/html/group__test__blocked__run__finalization__blocks__report__builder.html": 10456245679320672835, + "docs/api/html/group__OrchestratorCommands.html": 4670264817515095687, + "docs/api/html/group__test__dataset__review__scoped__command__routes__field__semantics__update.html": 453203095227910539, + "backend/git_repos/remote/test-repo.git/objects/d5/0298a42699f39e4601c0aa23eab07bcb0703c7": 1966124163198751340, + "venv/lib/python3.13/site-packages/_pytest/_py/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/json.py": 6429651405841289922, + "venv/lib/python3.13/site-packages/websockets/py.typed": 15130871412783076140, + "frontend/src/routes/datasets/MetricsTable.svelte": 4644516916752609053, + "backend/src/services/clean_release/repositories/manifest_repository.py": 12161285893423159929, + "backend/src/services/clean_release/dto.py": 16033619016556005561, + "venv/lib/python3.13/site-packages/pygments/formatters/bbcode.py": 8614391937525677922, + "docs/api/html/group__extract__tables__from__sql__span.html": 1554822379773857556, + "docs/api/html/group____make__sync__config__manager.html": 3168926697873935308, + "docs/api/html/group__TaskType.html": 10021229358037103244, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_index_as_string.py": 292438690259156869, + "docs/api/html/group__data__purity.html": 7580580678977879350, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/resource_protector.py": 4823504113176750140, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_asymmetric.py": 6903778603779934572, + "docs/api/html/test.html": 12181232123406114727, + "backend/src/services/reports/type_profiles.py": 9186322444310801411, + "docs/api/html/group__RootLayout.html": 12934665320478366323, + "venv/lib/python3.13/site-packages/numpy/__config__.py": 15794735729525094396, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs7.py": 9623344347519683527, + "backend/tests/services/clean_release/test_candidate_manifest_services.py": 18099141036073903381, + "docs/api/html/group__test__persist__task__resolves__environment__slug__to__existing__id.html": 12362861300562934210, + "backend/src/api/routes/assistant/_tool_maintenance.py": 16068777119321822609, + "venv/lib/python3.13/site-packages/jose/exceptions.py": 8562012933958941039, + "docs/api/html/group__test__api__duplicate__job__not__found.html": 18058786778280506982, + "backend/tests/scripts/test_clean_release_cli.py": 14028295526033195944, + "docs/api/html/group__test__candidate__manifest__services.html": 2472733304553637078, + "docs/api/html/group__TranslateDatasourcesApi.html": 16762087010624520764, + "docs/api/html/group__test__remove__nonexistent__validation__job.html": 7059450961386170424, + "docs/api/html/group__test__remove__validation__job.html": 9367627137821838796, + "docs/api/html/group__mask__pii__value.html": 9015692423384438989, + "docs/api/html/group__test__list__gitea__repositories__rejects__non__gitea.html": 8527146536000949913, + "docs/api/html/group__test__provision__adfs__user__new.html": 1392791487923839022, + "venv/lib/python3.13/site-packages/pyasn1/codec/native/__init__.py": 15728752901274520502, + "venv/lib/python3.13/site-packages/attr/__init__.py": 14436902698244718082, + "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/__init__.py": 4290669519959060720, + "venv/lib/python3.13/site-packages/sqlalchemy/exc.py": 6372950288664776625, + "backend/src/services/clean_release/compliance_orchestrator.py": 6747364300631405520, + "docs/api/html/group__RoleUpdate.html": 5923007426428235445, + "docs/api/html/group____fetch__batch__records.html": 733170840838093592, + "venv/lib/python3.13/site-packages/numpy/char/__init__.pyi": 12214207343146244743, + "frontend/src/components/TaskList.svelte": 17511781335293490363, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/meson.build": 17468163147765825687, + "venv/lib/python3.13/site-packages/apscheduler/executors/tornado.py": 6635576580306890718, + "venv/lib/python3.13/site-packages/attrs/converters.py": 13109003765349201809, + "venv/lib/python3.13/site-packages/pip/_internal/index/collector.py": 7454652626329467900, + "docs/api/html/search/groups_0.js": 14023012673265007156, + "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending.py": 16515485226251040454, + "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet_trash.py": 6191655521667576604, + "backend/git_repos/remote/test-repo.git/objects/00/fcf77cecdf261aef91eda792ab3384a3339922": 1527330822006618217, + "docs/api/html/group__GitPackage.html": 8185926952400552290, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py": 8577756801858495168, + "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.py": 4940170059946018356, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_to_offset.py": 3929599794655343911, + "docs/api/html/group__test__trigger__validation__creates__tasks.html": 10655556421929325211, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py": 7906029092844029127, + "venv/lib/python3.13/site-packages/charset_normalizer/api.py": 9172704759115759638, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_html.py": 7947998260347730581, + "backend/src/api/routes/dashboards/_router.py": 8641007051243802224, + "docs/api/html/group__StoresMock__Module.html": 1302505229892495674, + "docs/api/html/group__approve__batch__execution__mappings.html": 17647944553899662758, + "frontend/build/_app/immutable/nodes/8.j7J1hHPo.js": 3606828817468902929, + "venv/lib/python3.13/site-packages/pip/_internal/network/xmlrpc.py": 5064964778750366059, + "venv/lib/python3.13/site-packages/keyring/compat/properties.py": 1562771115138037410, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_convert.py": 77283525137564255, + "venv/lib/python3.13/site-packages/pygments/lexers/trafficscript.py": 776604244051744646, + "backend/src/models/storage.py": 8229531618855845590, + "venv/lib/python3.13/site-packages/pandas/io/clipboards.py": 5183212433608887727, + "venv/lib/python3.13/site-packages/charset_normalizer/version.py": 9948878594498587774, + "frontend/build/_app/immutable/nodes/19.Dop_BvBF.js": 1096918710287169866, "venv/lib/python3.13/site-packages/pip/_internal/commands/hash.py": 11824898770873127699, "docs/api/html/group____to__payload.html": 15947679921723097883, - "docs/api/html/group__PublicationService.html": 7781551738502534209, - "backend/src/plugins/translate/orchestrator_planner.py": 16598342286617456459, - "venv/lib/python3.13/site-packages/pydantic/_internal/_fields.py": 2692892815545218335, - "venv/lib/python3.13/site-packages/gitdb/test/lib.py": 13294941660283059872, - "venv/lib/python3.13/site-packages/passlib/win32.py": 5311393450489898748, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____updateSettings.html": 7369380578343660660, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_groupby.py": 16180421154734310873, - "docs/api/html/group__AuthModels.html": 12156417709466649370, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/__init__.py": 15130871412783076140, - "docs/api/html/group__test__text__only__model__returns__false.html": 3042385293654228545, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/console.py": 10217327186846649646, - "frontend/src/lib/stores/translationRun.js": 10005138082029605368, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-1.csv": 16078879475693341404, - "docs/api/html/group__test__get__report__not__found__returns__404.html": 6644055404803276631, - "docs/api/html/group__test__normalizeTranslateError__Function.html": 11445053005894736515, - "venv/lib/python3.13/site-packages/starlette/exceptions.py": 15978811612997200737, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/LICENSE": 8423899413955829149, - "docs/api/html/group__loadSettings__Function.html": 6940780943269890952, - "venv/lib/python3.13/site-packages/authlib/oidc/core/util.py": 5364455058429522633, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_constructors.py": 13890224577680389718, - "backend/git_repos/remote/test-repo.git/hooks/prepare-commit-msg.sample": 7530253302780245896, - "docs/api/html/group__TestAuth.html": 14086643015610504609, - "docs/api/html/group__get__user__dashboard__preference.html": 17414742887363265789, - "venv/lib/python3.13/site-packages/psycopg2_binary-2.9.11.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/annotated_doc/__init__.py": 6834198874252778425, - "venv/lib/python3.13/site-packages/apscheduler/executors/base.py": 1980520974511822101, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/control.py": 2034518223411945438, - "docs/api/html/group__BranchCheckout.html": 659397655908615531, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/parameters.py": 1647490964392525933, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_inference.py": 5741637509343419942, - "venv/lib/python3.13/site-packages/pygments/lexers/iolang.py": 10997512999728724130, - "docs/api/html/group__test__trigger__run__not__found.html": 13928430633871527174, - "docs/api/html/group__deleteTask__Function.html": 4139589046906492186, - "venv/lib/python3.13/site-packages/pip/_internal/locations/_sysconfig.py": 4773855595767526182, - "venv/lib/python3.13/site-packages/dotenv/version.py": 13508433229287636267, - "frontend/src/components/llm/ValidationReport.svelte": 17900215752976182981, - "docs/api/html/group__login__for__access__token.html": 5634643443334465635, - "venv/lib/python3.13/site-packages/jsonschema/protocols.py": 847332300543820834, - "venv/lib/python3.13/site-packages/numpy/_core/shape_base.pyi": 10225243791643474943, - "venv/lib/python3.13/site-packages/sqlalchemy/util/concurrency.py": 1325673503812009695, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/dtype.py": 9342837412280377086, - "venv/lib/python3.13/site-packages/pygments/lexers/_mql_builtins.py": 2376212048709413586, - "docs/api/html/group____strip__html__tags.html": 1085064762657441380, - "venv/lib/python3.13/site-packages/httpx/__version__.py": 9473642134297828811, - "backend/src/plugins/translate/__tests__/test_text_cleaner.py": 5456979246104056266, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_month.py": 8485374160446988786, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_astype.py": 4178231007649608531, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/authorization_code.py": 4958605540994687737, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_get.py": 10255752806860493777, - "docs/api/html/splitbar.png": 3792397713596451358, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_extending.py": 10202091354750153497, - "venv/lib/python3.13/site-packages/authlib/oauth1/client.py": 7314311571224771, - "docs/api/html/group__updateRole__Function.html": 935827486794561356, - "docs/api/html/group__getTasks__Function.html": 1086888913183663785, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_network.py": 13159427333040109523, - "docs/api/html/group__get__dashboard__layout.html": 16387331877683303616, - "venv/lib/python3.13/site-packages/PIL/PcfFontFile.py": 8867692961463798279, - "docs/api/html/group__ActivityTest__Module.html": 938547209662965960, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/globals_g.html": 13803013789474748050, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_kwarg.py": 10417724544218303733, - "venv/lib/python3.13/site-packages/dateutil/__init__.py": 17666544837260663800, - "backend/src/plugins/translate/__tests__/test_dictionary_filter.py": 2426844436521732941, - "venv/lib/python3.13/site-packages/numpy/random/_mt19937.cpython-313-x86_64-linux-gnu.so": 85866616759631810, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_comparison.py": 11333735187162513512, - "docs/api/html/group__test__idle__state.html": 1062794876151599472, - "docs/api/html/group__artifact__repository.html": 16366748896523529191, - "docs/api/html/globals_func.html": 10725622132015229383, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py": 5951912505933673835, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/sqlalchemy.py": 811968833735951956, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_legendre.py": 18266595775500927868, - "venv/lib/python3.13/site-packages/pandas/util/_decorators.py": 14780166101442507665, - "venv/lib/python3.13/site-packages/numpy/ma/testutils.pyi": 3899348061346041413, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_keys.py": 10813898277355150779, - "docs/api/html/group__frontend__src__services__git__utils__js____tryParseJsonObject.html": 8683269790796874276, - "docs/api/html/group__LLMResponseParser.html": 18031787096044719689, - "venv/lib/python3.13/site-packages/numpy/lib/_iotools.pyi": 12873243737872788349, - "backend/src/plugins/translate/_llm_parse.py": 3278211416813203155, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/_typing/_dtype_like.py": 1598511035344858524, - "docs/api/html/group__get__task__log__sources.html": 10195752095365161863, - "docs/api/html/group__SupersetContextTemplatesMixin____normalize__default__literal.html": 8062184899607586465, - "venv/lib/python3.13/site-packages/pandas/core/strings/object_array.py": 17631047188839635539, - "venv/lib/python3.13/site-packages/pyasn1/codec/ber/encoder.py": 8912119133548489544, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.py": 893743087939984792, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/test_array_with_attr.py": 6568496539994758985, - "venv/lib/python3.13/site-packages/git/refs/__init__.py": 16246686106073400519, - "venv/lib/python3.13/site-packages/pygments/lexers/esoteric.py": 16839499623020685146, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_math.h": 16701045088211015518, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/METADATA": 9586902338916246692, - "docs/api/html/group__test__should__log__task__level.html": 11553737108336190998, - "venv/lib/python3.13/site-packages/rapidfuzz/_common_py.py": 15410157970973169427, - "venv/lib/python3.13/site-packages/pip/_internal/network/__init__.py": 1586148254475575526, - "venv/bin/activate.csh": 14513616043256836238, - "docs/api/html/group__ProfilePermissionState.html": 771733666078465816, - "docs/api/html/group__test__parse__datetime__invalid__string.html": 3666486688843083256, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_is_homogeneous_dtype.py": 15378147393056707851, - "docs/api/html/group__ReportStatus.html": 11659907177460625683, - "venv/lib/python3.13/site-packages/pygments/token.py": 6987328478153445584, - "frontend/src/lib/auth/store.ts": 5831215136783173107, - "venv/lib/python3.13/site-packages/pygments/lexers/monte.py": 15158829991357994573, - "venv/lib/python3.13/site-packages/pandas/core/computation/align.py": 13027916530558901817, - "docs/api/html/group__SessionSummary.html": 13869105024431340740, - "docs/api/html/group__test__belief__scope__missing__anchor.html": 6431050729212027713, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/models.py": 3567900447243180991, - "venv/lib/python3.13/site-packages/pip/_internal/commands/index.py": 16715578868094112894, - "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.pyi": 6546751036506752550, - "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/test_uuid.py": 13955678427426850807, - "docs/api/html/group__test__enable__disable__schedule.html": 10584328518682693637, - "venv/lib/python3.13/site-packages/jose/utils.py": 1926138853970725083, - "venv/lib/python3.13/site-packages/numpy/conftest.py": 8971879716683736515, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/RECORD": 16874983218652404911, - "venv/lib/python3.13/site-packages/numpy/_core/cversions.py": 9724467539503410469, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/concat.py": 14737724508458849795, - "venv/lib/python3.13/site-packages/pygments/console.py": 3540660073442483692, - "venv/lib/python3.13/site-packages/pandas/core/interchange/column.py": 9498044216416177656, - "venv/lib/python3.13/site-packages/pygments/lexers/unicon.py": 1155454077052833821, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/WHEEL": 15858869568085970864, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_msvc.h": 14580207065003376324, - "backend/src/plugins/translate/__tests__/test_dictionary_import.py": 17094877730934121606, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8414/well_known.py": 7183795183137931709, - "venv/lib/python3.13/site-packages/numpy/_distributor_init.py": 1101690714183863290, - "venv/lib/python3.13/site-packages/jaraco/functools/__init__.py": 15334412919279565528, - "docs/api/html/group__extract__tables__from__sql.html": 8074608921519700652, - "docs/api/html/group__test__build__dataset__preview__query__context__merges__dataset__template__params__and__preserves__user__values.html": 13508623923930235675, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/criterion.py": 2042509334195547606, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_npfuncs.py": 6132674295193898715, - "frontend/src/routes/dashboards/[id]/components/DashboardHeader.svelte": 8694552424163588390, - "docs/api/html/group__test__authenticate__inactive__user.html": 8193931683359814732, - "docs/api/html/group__closeAssistantChat__Function.html": 5957962497908010406, - "docs/api/html/group__TASKS__DATABASE__URL.html": 7804801392493995117, - "docs/api/html/group__TranslationJob.html": 10104141994060655725, - "docs/api/html/group__tasks__Data.html": 8580763785347797438, - "docs/api/html/group__update__environment__schedule.html": 2300037495501071972, - "docs/api/html/group__logout__Function.html": 17141287000825844798, - "docs/api/html/group__test__plugin__loader__imports__logger.html": 4750311694643540001, - "venv/bin/pip3.13": 13861749540792881808, - "docs/api/html/group__frontend__src__services__taskService__js____resolveTask.html": 1005823194380786515, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/text.py": 6096215585499446070, - "venv/lib/python3.13/site-packages/anyio/_backends/_trio.py": 4764555436057754985, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/multiarray.pyi": 17232959648651278910, - "docs/api/html/group____ensure__user__dashboard__preferences__health__columns.html": 14728727752602035739, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/__init__.py": 1398617034174236358, - "backend/src/plugins/translate/__tests__/test_executor.py": 9030484360469714630, - "docs/api/html/group__test__get__dashboard__tasks__history__success.html": 3755590925245868067, - "docs/api/html/group__test__create__task__invalid__provider.html": 13091245729533197947, - "venv/lib/python3.13/site-packages/_pytest/runner.py": 5772610501857496741, - "venv/lib/python3.13/site-packages/requests/api.py": 12339139862411341381, - "venv/lib/python3.13/site-packages/numpy/_core/_asarray.py": 3468915833437229425, - "venv/lib/python3.13/site-packages/httpx/_main.py": 1812303705973265093, - "docs/api/html/group__MaintenanceStore__methods.html": 7821050348134458375, - "venv/lib/python3.13/site-packages/pygments/lexers/_stata_builtins.py": 18053452765493556047, - "venv/lib/python3.13/site-packages/pygments/styles/solarized.py": 11231143047572616298, - "frontend/src/routes/datasets/__tests__/split_view.test.js": 6143972529940917004, - "docs/api/html/group__loadFiles__Function.html": 12100105929385234165, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/RECORD": 16740224557818847141, - "venv/lib/python3.13/site-packages/pydantic/_internal/_generics.py": 16438159042939059717, - "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.py": 6348811074249398741, - "docs/api/html/group__createADGroupMapping__Function.html": 2891869775354387407, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_replace.py": 4953527309376861139, - "docs/api/html/group__NextExecutionResponse.html": 8976572706922461958, - "venv/lib/python3.13/site-packages/urllib3/util/wait.py": 14629837381061032546, - "venv/lib/python3.13/site-packages/pip/_internal/index/sources.py": 11609272011565024668, - "docs/api/html/group____update__yaml__file.html": 8696461266423546257, - "venv/lib/python3.13/site-packages/passlib/tests/sample1c.cfg": 13088198405762342506, - "venv/lib/python3.13/site-packages/uvicorn/__init__.py": 11517239807926185665, - "venv/lib/python3.13/site-packages/authlib/integrations/django_client/__init__.py": 14186136918454802843, - "docs/api/html/group__getTaskLogs__Function.html": 4009919543108782960, - "docs/api/html/group__require__api__key__or__jwt.html": 452602241730021960, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_two_greenlets.py": 11156275429806391390, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/__init__.py": 4752355175318979033, - "venv/lib/python3.13/site-packages/pandas/core/missing.py": 13332302791063948992, - "frontend/build/_app/immutable/chunks/DsnmJJEf.js": 17416107759626312632, - "backend/src/api/routes/assistant/_dataset_review.py": 4389080462745572364, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize_cpp.cpython-313-x86_64-linux-gnu.so": 7490281502544276964, - "backend/src/plugins/translate/__tests__/test_dictionary_utils.py": 2896829624338027530, - "venv/lib/python3.13/site-packages/pandas/core/tools/times.py": 6560679305255812809, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_arithmetic.py": 13304014482504911016, - "frontend/src/routes/dashboards/[id]/components/DashboardTaskHistory.svelte": 2668118032578627739, - "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.pyi": 1424133074692438241, - "venv/lib/python3.13/site-packages/pip/_internal/commands/configuration.py": 1608697090566485893, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_function_base.pyi": 10972502518588022706, - "venv/lib/python3.13/site-packages/pandas/tests/internals/test_internals.py": 2049484047553296860, - "docs/api/html/group__test__load__tasks__with__status__filter.html": 12175575007475513280, - "docs/api/html/group__MaintenanceSettingsUpdate.html": 5565369427863367370, - "docs/api/html/group__delete__dictionary__entry.html": 3989967080669519654, - "docs/api/html/group__test__html__only__content.html": 3771397076204846018, - "venv/lib/python3.13/site-packages/anyio/streams/stapled.py": 9733127303038397237, - "docs/api/html/group__test__execution__snapshot__skips__partial__imported__filters__without__values.html": 4065395894553398621, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/android.py": 8607703347015852195, - "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.pyi": 5892973170789971535, - "venv/lib/python3.13/site-packages/websockets/asyncio/server.py": 885415484558949506, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_add_prefix_suffix.py": 8367793476246001244, - "docs/api/html/group__I18nModule.html": 3811966300603139836, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_no_deprecated_api.h": 7896028550808252043, - "docs/api/html/group__TaskResultPanel.html": 4837205169025137611, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/METADATA": 11759004913807981650, - "docs/api/html/group__gitServiceContractTests__Module.html": 15217057486548108664, - "docs/api/html/group__ValidationTaskService.html": 42675372436809990, - "venv/lib/python3.13/site-packages/fastapi/dependencies/utils.py": 7687214523082385505, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.pyi": 5269136485429780038, - "docs/api/html/group__publish__candidate__endpoint.html": 8010913513962824406, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_day.py": 2200008998040663785, - "docs/api/html/group__ReportDetailUxTest__Module.html": 17811262593949560202, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_construction.py": 6564829381134214401, - "docs/api/html/group__SupersetClientExtractChartIdsFromLayout.html": 397319256700010303, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/cache_key.py": 11118819065204404106, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py": 15726448028057859831, - "venv/lib/python3.13/site-packages/pygments/lexers/thingsdb.py": 6073447481627395832, - "docs/api/html/group__report__service.html": 3161527402315470112, - "docs/api/html/group__list__execution__mappings.html": 5449792529029019677, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64-testset-2.csv": 5423145402839576695, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/lib/__init__.py": 12211229657513986410, - "docs/api/html/group__TranslateRoutes.html": 12674068421448873561, - "docs/api/html/group__RunListResponse.html": 11273163658263807436, - "venv/lib/python3.13/site-packages/rapidfuzz/__pyinstaller/test_rapidfuzz_packaging.py": 5189055608193366093, - "venv/lib/python3.13/site-packages/starlette/middleware/wsgi.py": 12506262030845301606, - "docs/api/html/group__handleUpload__Function.html": 10159956200065592690, - "docs/api/html/group____discover__route__permissions.html": 6994842999221083334, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/const_vs_enum.py": 12873459583943748931, - "docs/api/html/group__FeedbackRequest.html": 5229199263614285726, - "docs/api/html/group__frontend__src__routes__settings____page__ts____load.html": 17232811554336351206, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_string.py": 11422235699126032386, - "venv/lib/python3.13/site-packages/pandas/_libs/interval.pyi": 15698173755300703836, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_unix.h": 9608771997528633681, - "venv/lib/python3.13/site-packages/numpy/core/records.py": 12604652690721760437, - "docs/api/html/group__test__page__size__constant__exists.html": 5309175304176573078, - "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.py": 338595621218646492, - "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.pyi": 8195943561787777127, - "docs/api/html/search/functions_0.js": 18275711831096102872, - "venv/lib/python3.13/site-packages/referencing/_attrs.py": 14434891919747863272, - "docs/api/html/group__test__parse__superset__link__dashboard__permalink__recovers__dataset__from__nested__dashboard__state.html": 16106300672165677177, - "docs/api/html/group__TaskModels.html": 17614003799808664482, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.pyi": 15130871412783076140, - "docs/api/html/group__CotLoggerModule.html": 10976879847879957078, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_datetimes.py": 7889573945417063183, - "venv/lib/python3.13/site-packages/pandas/tseries/frequencies.py": 2262381949755250516, - "docs/api/html/group__CorrectionCellTests.html": 16634413555411031846, - "venv/lib/python3.13/site-packages/typing_inspection/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/dateutil/tz/_factories.py": 7538654189286887518, - "backend/src/services/clean_release/repositories/publication_repository.py": 1097065694003039928, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_interval_array_equal.py": 15654625365223535344, - "backend/src/api/routes/llm.py": 14720807640771498486, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/licenses/LICENSE": 3868190977070717994, - "venv/lib/python3.13/site-packages/pip/_internal/cli/cmdoptions.py": 12107733000019393631, - "venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py": 10408538054406276433, - "docs/api/html/group__TranslationMetrics.html": 4038252507282181238, - "docs/api/html/group__SettingsSchemas.html": 7781964904235893244, - "venv/lib/python3.13/site-packages/click/formatting.py": 6694583529073729642, - "venv/lib/python3.13/site-packages/pydantic/_internal/_repr.py": 1758478941817868344, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_round.py": 9358309072340573675, - "docs/api/html/group__AppTimezone.html": 1905496000801051257, - "docs/api/html/group__SourceIsolation.html": 15074253220325240781, - "docs/api/html/group__PasswordPrompt.html": 16889108354807031960, - "backend/src/dependencies.py": 3093520809371792884, - "docs/api/html/group____accumulate__objects.html": 12340985983036661997, - "venv/lib/python3.13/site-packages/pandas/_libs/groupby.cpython-313-x86_64-linux-gnu.so": 14931394220112658268, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_setops.py": 9991233160569430204, - "docs/api/html/group__GitRepoOperationsRoutes.html": 17274671154448200931, - "venv/lib/python3.13/site-packages/fastapi/middleware/cors.py": 7452860773712601508, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/RECORD": 9803861100978586236, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_series.py": 17126297747681689408, - "venv/lib/python3.13/site-packages/passlib/ext/__init__.py": 15240312484046875203, - "docs/api/html/group__get__detector.html": 10160834986755311488, - "docs/api/html/group__DashboardNotFoundError.html": 3334527199488317700, - "docs/api/html/doxygen.css": 8786819515706830953, - "backend/tests/test_deprecation_audit_fixes.py": 17902995210831708431, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_dtypes_basic.py": 10917162988625838981, - "venv/lib/python3.13/site-packages/keyring/compat/properties.py": 1562771115138037410, - "docs/api/html/group__test__validate__path__already__exists.html": 13732390823029512954, - "docs/api/html/navtreeindex10.js": 1322788531203602791, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/RECORD": 7363618870628306370, - "backend/src/services/clean_release/stages/internal_sources_only.py": 6150576100887638944, - "docs/api/html/group____extract__id.html": 3759315153888823869, - "docs/api/html/group__MergeContinueRequest.html": 15873066823531580576, - "venv/lib/python3.13/site-packages/pandas/core/computation/eval.py": 3176226283265954028, - "docs/api/html/group____flush__task__logs.html": 9037017981358195543, - "docs/api/html/group____serialize__clarification__question__payload.html": 2145294211221341001, - "venv/lib/python3.13/site-packages/jsonschema/__main__.py": 18136546137047911521, - "docs/api/html/group__loadGiteaRepos__Function.html": 6310672505465620281, - "docs/api/html/group__NavigationMock.html": 10866560030441854001, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 12574929384355254457, - "venv/lib/python3.13/site-packages/apscheduler/triggers/date.py": 9545774812177853725, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.cpython-313-x86_64-linux-gnu.so": 17089839649971421164, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_format.py": 1947585389944838076, - "docs/api/html/group__TranslateJobCreate.html": 13885891459376922086, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/errors.py": 16711622442922413520, - "venv/lib/python3.13/site-packages/passlib/utils/compat/__init__.py": 17369882236606423129, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/hstore.py": 9700253004947691961, - "docs/api/html/group__get__my__preference.html": 12554124199014209894, - "venv/lib/python3.13/site-packages/psycopg2/_ipaddress.py": 12332672835751787869, - "venv/lib/python3.13/site-packages/_pytest/terminalprogress.py": 5490989904517343259, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/astype_copy.pkl": 4470155972891419163, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_api.py": 14120915875574640262, - "docs/api/html/group__build__impact__summary.html": 17415672638957538101, - "docs/api/html/group__test__us3__mapping__patch__approval__preview__and__launch__endpoints.html": 12512918894261325848, + "docs/api/html/group__docker__backend__entrypoint.html": 6509924598611355, "venv/lib/python3.13/site-packages/pydantic/_internal/_core_metadata.py": 5704137311404303574, - "docs/api/html/group__frontend__src__services__adminService__js____deleteUser.html": 16139969298187481622, - "docs/api/html/group__test__storage__model__compiles.html": 12703678636634941542, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/METADATA": 4868544280955034757, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_complex.py": 11732242536199439023, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_convert_dtypes.py": 13612212066710266457, - "venv/lib/python3.13/site-packages/pygments/styles/trac.py": 13968346768164381833, - "docs/api/html/group__getStatusBadgeClass__Function.html": 12235710533832862804, - "docs/api/html/group__run__cleanup.html": 10946982331286352210, - "venv/lib/python3.13/site-packages/pydantic/v1/datetime_parse.py": 10467114625921924095, - "frontend/src/routes/admin/roles/+page.svelte": 7687872545536151950, - "frontend/src/lib/api/translate/__tests__/test-target-schema.test.js": 16872327388398551554, - "docs/api/html/group__test__invalid__add__log__fn.html": 13488633198272059949, - "docs/api/html/group__test__get__dashboard__thumbnail__env__not__found.html": 2691256043030470661, - "docs/api/html/group__DatabaseModule.html": 14601049603487173809, - "docs/api/html/group__MissingMappingModal.html": 3905855008524248166, - "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.pyi": 16573082220135508510, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/RECORD": 8956798822782402202, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_halfyear.py": 9376272342364384767, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/METADATA": 12295263493152296681, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_reductions.py": 3575617237516305456, - "docs/api/html/group__fetch__models.html": 1931603908808447028, - "venv/lib/python3.13/site-packages/sqlalchemy/event/attr.py": 5971663429221923939, - "venv/lib/python3.13/site-packages/pygments/lexers/ldap.py": 17455983150181177257, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/ecdsa/test_numbertheory.py": 2117130219401053225, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_round.py": 7912161077874611432, - "venv/lib/python3.13/site-packages/pip/_internal/utils/entrypoints.py": 11268059714209889385, - "venv/lib/python3.13/site-packages/numpy/_core/__init__.pyi": 2783897020308040960, - "venv/lib/python3.13/site-packages/numpy/lib/mixins.py": 7404729635424818497, - "backend/tests/test_layout_utils.py": 15634672279171875732, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_regression.py": 4611768764484593979, - "venv/lib/python3.13/site-packages/pandas/tests/extension/list/test_list.py": 4538625980063375611, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/__init__.py": 63497920264089252, - "docs/api/html/group__extract__owner__identifiers.html": 8294813531215300582, - "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.pyi": 10077265709692546934, - "frontend/src/lib/api/validation.js": 10650815174196870103, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/plugin_base.py": 2880434680251108786, - "docs/api/html/group__TerminologyDictionary.html": 430268445994358090, - "docs/api/html/group__test__api__key__auth__valid.html": 3238968940529899361, - "docs/api/html/group__TranslationE2E.html": 15552322666790950919, - "backend/src/models/dataset_review_pkg/_session_models.py": 9902104970008253748, - "backend/src/plugins/translate/__tests__/test_orchestrator.py": 10313792176402719801, - "venv/lib/python3.13/site-packages/attr/exceptions.pyi": 12323493441583681402, - "docs/api/html/group__build__violation.html": 1522726666566321384, - "docs/api/html/group__TemplateVariable.html": 1861586953213650229, - "venv/lib/python3.13/site-packages/pydantic/_internal/_namespace_utils.py": 5237766366340181382, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_drop.py": 11341278355685583133, - "venv/lib/python3.13/site-packages/pip/_internal/locations/base.py": 7966628150470073365, - "docs/api/html/group__resolve__task__type.html": 14911663331335466278, - "docs/api/html/group____persist__confirmation.html": 1641246192766111863, - "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimes.py": 14331984919364185787, - "venv/lib/python3.13/site-packages/pandas/util/_print_versions.py": 15326663505771588740, - "frontend/src/services/gitService.js": 6764482702653031888, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py": 3177663126928467950, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/INSTALLER": 17282701611721059870, - "frontend/build/_app/immutable/nodes/12.Dup5w6id.js": 2480806793952674044, - "venv/lib/python3.13/site-packages/numpy/tests/test_matlib.py": 14395481530627870882, - "frontend/build/_app/immutable/nodes/34.Dxv3smSb.js": 4631280726068973380, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/regexopt.py": 16073437303824961930, - "docs/api/html/group__LoggingConfigResponse.html": 16867400746359802764, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arithmetic.pyi": 10265823318697442495, - "docs/api/html/group____estimate__markdown__height.html": 16586606759606629897, - "docs/api/html/navtreeindex5.js": 5360518571003728409, - "docs/api/html/group__update__metric__description.html": 15940615433146474716, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_duplicated.py": 14676058112867601208, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reshape.py": 10576230966727810832, - "docs/api/html/navtreeindex8.js": 16014422725768627041, - "docs/api/html/group__GitRepository.html": 11611249034372487005, - "docs/api/html/group__test__baseline__expired__fallback.html": 15287350939065441355, - "venv/lib/python3.13/site-packages/pip/_internal/cli/index_command.py": 15798187258841839292, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.f": 18215492757907465667, - "docs/api/html/group__toggleAssistantChat__Function.html": 16851022063245577373, - "venv/lib/python3.13/site-packages/authlib/jose/drafts/_jwe_enc_cryptodome.py": 15753279711312689193, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.cpython-313-x86_64-linux-gnu.so": 5441755022198996487, - "docs/api/html/group__DatasetReviewWorkspace.html": 16920997955167704434, - "docs/api/html/search/all_17.js": 369213695750280983, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine.py": 4446011286411407708, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_delete.py": 5958440386632506587, - "backend/src/services/reports/__tests__/test_report_normalizer.py": 5081647081006613913, - "docs/api/html/group__MaintenanceStoreTests.html": 14540516874541556957, - "frontend/src/routes/datasets/ColumnsTable.svelte": 14113713230812728863, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_libgroupby.py": 9985769511561272200, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/METADATA": 6557531107125553027, - "frontend/build/_app/immutable/chunks/CMoL_NLs.js": 17187013106646297092, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/modeline.py": 6194365452131561478, - "venv/lib/python3.13/site-packages/_pytest/unraisableexception.py": 4597178803320514675, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/pubprivmod.f90": 8680230823849248800, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/urllib3/http2/connection.py": 1661620860881397554, - "docs/api/html/group__test__detect__language__edge__cases.html": 18380807334063203499, - "docs/api/html/group__delete__job.html": 992976939561111352, - "venv/lib/python3.13/site-packages/click/_compat.py": 14451519072991457993, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_isfile.py": 14544060093268573418, - "backend/src/core/migration/__init__.py": 13075189211840127611, - "venv/lib/python3.13/site-packages/pandas/_libs/ops.pyi": 1192312467209548763, - "docs/api/html/group__test__migrate__dashboards__no__ids.html": 5314005886091290809, - "docs/api/html/group__update__logging__config.html": 18321262860475642376, - "docs/api/html/group__register__adfs.html": 7944423328544762483, - "docs/api/html/group__storage__routes.html": 18273179632379125125, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_log_render.py": 18076931145915444473, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_constructors.py": 12118296571694122743, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/statreload.py": 5018382301336802445, - "venv/lib/python3.13/site-packages/pygments/lexers/automation.py": 8021926838024836904, - "venv/lib/python3.13/site-packages/_pytest/doctest.py": 6126919596776592278, - "venv/lib/python3.13/site-packages/pygments/styles/nord.py": 8469538903422182155, - "frontend/src/lib/stores/__tests__/mocks/state.js": 10691107564567912815, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_csv.py": 10737444103906389448, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_get.py": 6509102681661636928, - "docs/api/html/group__MappingWarningLevel.html": 16869836344481921740, - "frontend/build/_app/immutable/chunks/BeYMLtlT.js": 8363179184295836132, - "frontend/src/lib/api/translate/datasources.js": 17202165717960970061, - "docs/api/html/group__manifest__consistency.html": 302557474349744915, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/abc.py": 6227782083136374888, - "venv/lib/python3.13/site-packages/pygments/lexers/business.py": 10410439210806697214, - "venv/lib/python3.13/site-packages/pytest/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_overlap.py": 4202716587079637350, - "docs/api/html/group__SupersetClientBase.html": 11179460536607971200, - "docs/api/html/group__test__chart__creation__failure.html": 15228478008422010264, - "venv/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py": 8145740626773638218, - "venv/lib/python3.13/site-packages/pandas/_testing/_io.py": 14264259716673715185, - "docs/api/html/group__frontend__src__lib__stores__health__js____createHealthStore.html": 18071199202062130010, - "docs/api/html/group__test__calculate__schedule__single__task.html": 12252381417874237319, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/schema.py": 8358664670620586363, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.obj": 2884737706935637437, - "venv/lib/python3.13/site-packages/sqlalchemy/util/__init__.py": 15821192733097665649, - "docs/api/html/group__frontend__src__services__taskService__js____resumeTask.html": 2730926071021487540, - "venv/lib/python3.13/site-packages/pygments/lexers/basic.py": 12723021007042054402, - "venv/lib/python3.13/site-packages/pygments/lexers/webmisc.py": 9530962803076253849, - "frontend/src/lib/components/ui/MultiSelect.svelte": 1475503303018683038, - "docs/api/html/group__remove__validation__job.html": 18264277450501764734, - "frontend/src/components/llm/__tests__/provider_config.integration.test.js": 2807487384272452401, - "backend/src/core/ws_log_handler.py": 16227392517834423362, - "backend/git_repos/remote/test-repo.git/objects/38/0395343915d386a8d58774cc6c6677205065e8": 5481382229802988191, - "venv/lib/python3.13/site-packages/pydantic/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/httpcore/_sync/connection.py": 1539337414493486168, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpp": 4590574206187223887, - "venv/bin/httpx": 6651007446865716794, - "docs/api/html/tabs.css": 16739738225401062757, - "venv/lib/python3.13/site-packages/PIL/ImageDraw.py": 12919304671639673744, - "venv/lib/python3.13/site-packages/anyio/_core/_signals.py": 1204322949639513552, - "docs/api/html/group__SessionEvent.html": 1211750384704363849, - "docs/api/html/group__AsyncAPIClient____normalize__base__url.html": 18252709079294820175, - "docs/api/html/search/groups_14.js": 15467890647915855647, - "venv/lib/python3.13/site-packages/pydantic_core/_pydantic_core.pyi": 16799580742094025110, - "docs/api/html/globals_func_l.html": 2863216576558791276, - "docs/api/html/pages.html": 4338336449088201146, - "venv/lib/python3.13/site-packages/pandas/_libs/json.cpython-313-x86_64-linux-gnu.so": 2436285663630821963, - "docs/api/html/group__SupersetDashboardsFiltersMixin.html": 17578063671731484965, - "docs/api/html/group__ReviewWorkspaceHelpers.html": 1176940369643831408, - "docs/api/html/group__ValidationFindingsPanelUxTests__Module.html": 3492500435759183400, - "docs/api/html/group__EnvironmentsApi.html": 15457246866592390632, - "venv/lib/python3.13/site-packages/pygments/lexers/teraterm.py": 16132676290138105984, - "docs/api/html/group__verifySessionAndAccess__Function.html": 6518276629640187972, - "docs/api/html/group__BackupsRedirectPage.html": 2891216165677443076, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/METADATA": 10810011740984652381, - "venv/lib/python3.13/site-packages/numpy/_pytesttester.py": 17737911110576747508, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_promote.py": 16601872688404846307, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_period.py": 6047041810867211174, - "venv/lib/python3.13/site-packages/_yaml/__init__.py": 3395829783751571350, - "venv/lib/python3.13/site-packages/pygments/lexers/jslt.py": 11805394130669644345, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/WHEEL": 18203500019759199992, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/licenses/LICENSE": 3868190977070717994, - "venv/lib/python3.13/site-packages/greenlet/tests/_test_extension_cpp.cpython-313-x86_64-linux-gnu.so": 7659849621107414188, - "docs/api/html/group__format__timezone__offset.html": 14635780330486688343, - "venv/lib/python3.13/site-packages/pygments/lexers/wowtoc.py": 17527844575999013592, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23879.f90": 13174951794239860144, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_api.py": 13319208721442328400, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/types.py": 356973910046950951, - "venv/lib/python3.13/site-packages/pygments/lexers/_lilypond_builtins.py": 1766946089314631488, - "venv/lib/python3.13/site-packages/pygments/lexers/mosel.py": 1877592344012288438, - "docs/api/html/group__sync__environment.html": 1811187775036019320, - "docs/api/html/group__build__namespaced__id.html": 12150766799543768530, - "docs/api/html/group__TestCleanReleaseApi.html": 16185754403987215515, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.pyi": 15789956857032062129, - "docs/api/html/group__startDryRun__Function.html": 1798526875933461819, - "docs/api/html/group__transform__zip.html": 10792708138270698712, - "docs/api/html/group__delete__running__tasks.html": 1080036419250840372, - "backend/src/plugins/llm_analysis/service.py": 8630918629937474981, - "docs/api/html/group____dispatch__intent.html": 9089068514794356054, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_assignability.pyi": 1718610984396277997, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_dialect.py": 4700488504252803159, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/decl_api.py": 5287018911951723034, - "docs/api/html/group__DatasetReviewDependencies.html": 2190246894914856689, - "docs/api/html/group__RegistryStatus.html": 11698693702774238014, - "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.pyi": 2628035888703031029, - "venv/lib/python3.13/site-packages/pip/_internal/utils/setuptools_build.py": 13594639612360346306, - "docs/api/html/group__check__api__key__environment__scope.html": 12869540980840565823, - "backend/src/services/clean_release/approval_service.py": 14346840951852454312, - "docs/api/html/group__MockConfigManager.html": 2192424076609045256, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_categorical.py": 9923236395692943732, - "docs/api/html/group__get__task.html": 2469252830729210519, - "venv/lib/python3.13/site-packages/numpy/f2py/__main__.py": 12567751646090102167, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_where.py": 3272402211717453881, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/orderinglist.py": 10162099108656794338, - "docs/api/html/navtreeindex21.js": 7533748426858788437, - "docs/api/html/group__test__get__job__metrics.html": 1706423668878190645, - "venv/lib/python3.13/site-packages/jsonschema/_utils.py": 14237077105384631097, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/align.py": 6078666505647454830, - "backend/src/scripts/seed_superset_load_test.py": 7430188463595432869, - "docs/api/html/group__LLMProvider.html": 9637931473416605279, - "docs/api/html/group__TestCleanReleaseModels.html": 7307930441105961889, - "docs/api/html/group__DeleteRunningTasksUtil.html": 11213940847453331921, - "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_gen_files.py": 11149423183253618378, - "frontend/playwright-report/trace/uiMode.C2Efnu2P.js": 2717171022864487300, - "docs/api/html/group__test__extract__native__filters__from__permalink.html": 12643899760315874197, - "frontend/build/_app/immutable/chunks/rzyp_Wbp.js": 10559672264716197308, - "docs/api/html/group__update__maintenance__settings.html": 17618732781129600578, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_simd.py": 7577408467041941595, - "backend/src/core/utils/superset_context_extractor/_base.py": 6349561308410782726, - "docs/api/html/group__data__purity.html": 7580580678977879350, - "venv/lib/python3.13/site-packages/_pytest/outcomes.py": 12782895027812738142, - "venv/lib/python3.13/site-packages/websockets/legacy/exceptions.py": 16025779645869134942, - "docs/api/html/group__test__api__key__auth__environment__scope__ok.html": 15499030741410944952, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_ufunc.py": 3326502989311831856, - "docs/api/html/search/groups_4.js": 16921846554525899660, - "docs/api/html/group__log__security__event.html": 18315452659527006860, - "backend/src/plugins/__init__.py": 17194112070692432889, - "docs/api/html/group__list__jobs.html": 7585793809542090283, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/converter.py": 9041278869295701566, - "backend/src/api/routes/__tests__/test_assistant_authz.py": 3835028453926095879, - "docs/api/html/group__extract__data__rows.html": 16073979208553120843, - "frontend/src/routes/datasets/StatsBar.svelte": 4241697609925045735, - "frontend/src/routes/login/+page.svelte": 2655837067927461296, - "docs/api/html/group____build__target__signatures.html": 16982125749763522469, - "venv/lib/python3.13/site-packages/pygments/lexers/ada.py": 17032408976634308999, - "docs/api/html/group__TokenEstimator.html": 12075103575602311347, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_utils.py": 4612923732868929770, - "venv/lib/python3.13/site-packages/cryptography/x509/general_name.py": 4481582226129488535, - "venv/lib/python3.13/site-packages/typing_inspection/introspection.py": 13916695705179237047, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame.py": 16757570551941104401, - "docs/api/html/group__FetchModelsRequest.html": 12761345404730870015, - "venv/lib/python3.13/site-packages/pandas/_libs/index.pyi": 17679349633640606985, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_libsparse.py": 1859997684694949029, - "frontend/src/routes/settings/automation/+page.svelte": 13199339263146300897, - "docs/api/html/group__test__delete__task__success.html": 6168518344728381187, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/resource_protector.py": 4823504113176750140, - "docs/api/html/group__CheckFinalStatus.html": 8660527585364518838, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/bar.py": 12859947592168238832, - "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.pyi": 3661113433078225952, - "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.py": 6205675182996213576, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_missing.py": 1963705411068820866, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/env.py": 1254612137164491329, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_numpy_array_equal.py": 2089426719574307767, - "docs/api/html/group__register__candidate__v2__endpoint.html": 5175001187205682785, - "venv/lib/python3.13/site-packages/attr/_next_gen.py": 16353246941851015420, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nditer.pyi": 2351219156711800159, - "docs/api/html/group__duplicateTask__Function.html": 6572424327379812707, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_apply.py": 7511915990531229598, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/__init__.py": 16434403703599680307, - "backend/src/api/routes/maintenance/_routes.py": 18203881358318110485, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/WHEEL": 9788895026336324100, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/linalg.pyi": 269481015135399748, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_arithmetic.py": 17427754419626622853, - "backend/src/services/clean_release/enums.py": 5838176998590033780, - "backend/tests/test_maintenance_service.py": 6347730951072309013, - "backend/src/core/__tests__/test_superset_preview_pipeline.py": 13996819877413389729, - "backend/src/api/routes/__tests__/test_migration_routes.py": 12273917934344716029, - "docs/api/html/group__fetchRuns__Function.html": 10568246588417794919, - "venv/lib/python3.13/site-packages/pandas/io/excel/_xlrd.py": 14430937200583666658, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nested_sequence.pyi": 11245460325970663922, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_empty.py": 13006241382741353569, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_mask.py": 9654897363729130542, - "venv/lib/python3.13/site-packages/pip/_internal/commands/check.py": 6937252393012787214, - "docs/api/html/group__ASSISTANT__AUDIT.html": 13579278608656856819, - "docs/api/html/group__update__preferences.html": 18008584156403722342, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml.py": 6906547816798138000, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_table.tpl": 8706980975714184465, - "frontend/src/routes/settings/git/+page.svelte": 13757324397807811651, - "venv/lib/python3.13/site-packages/pydantic/_migration.py": 17982816821930887921, - "venv/lib/python3.13/site-packages/pandas/io/stata.py": 14370692318835340323, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_block_docstring.py": 18146459246837890489, - "venv/lib/python3.13/site-packages/numpy/f2py/diagnose.pyi": 8607205990821121708, - "venv/lib/python3.13/site-packages/_pytest/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/ma/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__Base.html": 12380905760850821737, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_logical.py": 6242457515917145500, - "backend/src/services/notifications/service.py": 3120728755335117444, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_datetime.py": 3499461736474530062, - "venv/lib/python3.13/site-packages/PIL/ImageDraw2.py": 13610162107593278891, - "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.py": 4984721349618127021, - "docs/api/html/group__test__provision__adfs__user__existing.html": 3869094630531615689, - "venv/lib/python3.13/site-packages/rsa/__init__.py": 14623742580410014505, - "docs/api/html/group__test__resolve__trusted__policy__snapshots__missing__profile.html": 13528681884996041677, - "venv/lib/python3.13/site-packages/anyio/functools.py": 4300781896135050327, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/__init__.py": 15130871412783076140, - "frontend/src/components/Footer.svelte": 12838918285533328222, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_arithmetic.py": 16829312896275893002, - "docs/api/html/doc.svg": 6703136184032142780, - "docs/api/html/group__test__belief__scope__not__visible__at__info.html": 7650312905986638988, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_deprecate_nonkeyword_arguments.py": 11676809974120007408, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertsql.py": 4633484475514351889, - "venv/lib/python3.13/site-packages/pygments/lexers/perl.py": 17014528337020572681, - "docs/api/html/group__frontend__src__lib__stores__sidebar__js____toggleSidebar.html": 1585062636957734877, - "docs/api/html/group__test__batch__detect.html": 9632306453893154983, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/measure.py": 18327922742808984428, - "backend/src/services/reports/__tests__/test_type_profiles.py": 8045459741015920365, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/METADATA": 11439969143853665311, - "docs/api/html/group__test__list__tasks__success.html": 9673959434282998524, - "venv/lib/python3.13/site-packages/pandas/io/formats/printing.py": 3054136648863371285, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/LICENSE": 6989336036499641077, - "docs/api/html/group__SupersetDashboardsWriteMixin.html": 8613916271231427973, - "docs/api/html/group__test__missing__secret__key__crashes.html": 8704844476215085040, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/websockets/sync/__init__.py": 15130871412783076140, - "docs/api/html/group__test__get__remote__ids__batch__returns__empty__for__empty__input.html": 5603538648054911229, - "backend/tests/test_storage_audit_fixes.py": 6119862973635892572, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____patchSession.html": 5561812461916057904, - "docs/api/html/tab_b.png": 17139676106204569061, - "docs/api/html/group__test__extract__chart__uuids__from__archive.html": 3048505118835977338, - "venv/lib/python3.13/site-packages/PIL/Jpeg2KImagePlugin.py": 16905402423248975204, - "docs/api/html/group__remove__empty__directories.html": 14528085660669051293, - "docs/api/html/group__build__rows.html": 5075247591388659502, - "docs/api/html/group__listFiles__Function.html": 10223277943043127993, - "venv/lib/python3.13/site-packages/httpx/_urls.py": 4219389176352582046, - "venv/lib/python3.13/site-packages/numpy/_core/defchararray.py": 15550230459945996915, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.py": 2401931196871484449, - "venv/lib/python3.13/site-packages/pydantic/plugin/_loader.py": 792663962985957671, - "docs/api/html/group__StoragePlugin.html": 1518633266255703440, - "venv/lib/python3.13/site-packages/numpy/_pytesttester.pyi": 5750506937526308405, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/tornado.py": 6491136785917169991, - "docs/api/html/group__DocumentationPlugin.html": 4602413971047524450, - "backend/tests/test_resource_hubs.py": 1620639151885021568, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/providers.py": 7267821734584612032, - "frontend/src/components/backups/BackupManager.svelte": 15318298322895797591, - "venv/lib/python3.13/site-packages/authlib/oidc/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/tablegen.py": 2917626373516779857, - "docs/api/html/group__frontend__src__lib__api__translate__target__schema__js____checkTargetTableSchema.html": 12580964374623879461, - "docs/api/html/group__test__approve__rejects__duplicate__approve.html": 11531613578978927773, - "docs/api/html/group__TranslateHistoryTests.html": 16053454222263447135, - "docs/api/html/group__delete__repository.html": 16831781850407059770, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc64_aix.h": 8706347224105630468, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ndarray_misc.py": 8706265665356094801, - "venv/lib/python3.13/site-packages/pandas/io/json/__init__.py": 12333505155070172626, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/METADATA": 10707390829735099042, - "frontend/src/lib/components/MaintenanceEventsTable.svelte": 1697386217337210174, - "backend/src/api/routes/health.py": 13097980848666987664, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/strategies.py": 15673926142454922581, - "docs/api/html/group__test__map__columns__invalid__source__type.html": 8185525188861927511, - "venv/lib/python3.13/site-packages/keyring/devpi_client.py": 3445890176686211982, - "docs/api/html/group__id.html": 4811327565109647473, - "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/__init__.py": 16416360448593978041, - "docs/api/html/group____is__conversation__archived.html": 601991753846203755, - "docs/api/html/group__get__password__hash.html": 1918521256255854564, - "docs/api/html/group____build__validation__export.html": 10674121092394091734, - "docs/api/html/group__frontend__src__services__adminService__js____createUser.html": 12401425458929275331, - "docs/api/html/group__StoragePluginPackage.html": 6430288179557924153, - "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.hpp": 6366432579621298231, - "venv/lib/python3.13/site-packages/passlib/handlers/pbkdf2.py": 16686685453577026643, - "backend/src/api/routes/dashboards/_projection.py": 9178951020532547334, - "venv/lib/python3.13/site-packages/numpy/_globals.pyi": 12839879732484756979, - "docs/api/html/group____serialize__clarification__state.html": 16624968641905184426, - "docs/api/html/group__SupersetContextExtractorBase______init____.html": 1177619611547833454, - "docs/api/html/group__handleCancel__Function.html": 3411761753976161711, - "venv/lib/python3.13/site-packages/pydantic/deprecated/config.py": 12280996227531856230, - "venv/lib/python3.13/site-packages/PIL/JpegImagePlugin.py": 10621639599916796689, - "docs/api/html/group__NativeFilterExtractionTests.html": 14602614888501976510, - "docs/api/html/group__DashboardSchemas.html": 17302805929430757920, - "docs/api/html/group__ValidationRunResponse.html": 17135497457065164402, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_quantile.py": 11966156706220948315, - "venv/lib/python3.13/site-packages/pygments/styles/murphy.py": 8177992406082781615, - "venv/lib/python3.13/site-packages/git/refs/remote.py": 4391983355222612205, - "docs/api/html/group__DatasetReviewSessionModels.html": 15486238525446211823, - "docs/api/html/group__DashboardHub__formatDate__Function.html": 6545488449073827576, - "docs/api/html/group__test__list__tasks__service__error.html": 1075541340524463072, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/specifiers.py": 12284901056355482991, - "venv/lib/python3.13/site-packages/pygments/lexers/futhark.py": 10102535873433759899, - "backend/src/api/routes/dataset_review_pkg/_dependencies.py": 5445855387226126427, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/introspection.py": 14645217065508644154, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py": 6509914623926583629, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/__init__.py": 12793458753561114666, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/rule.py": 5318344538761246105, - "venv/lib/python3.13/site-packages/yaml/emitter.py": 6036657460416912844, - "docs/api/html/group__TranslateSchemas.html": 17296673720360831542, - "docs/api/html/search/all_14.js": 8771148315983143295, - "venv/lib/python3.13/site-packages/pandas/core/interchange/utils.py": 11401792151107207834, - "venv/lib/python3.13/site-packages/git/exc.py": 14278238357119816365, - "frontend/src/lib/stores/__tests__/setupTests.js": 16228706107748081093, - "backend/src/services/__tests__/test_health_service.py": 16470443406586362974, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_datetimelike.py": 10294379314868108649, - "venv/lib/python3.13/site-packages/pygments/lexers/asm.py": 13861952972248758785, - "docs/api/html/group____map__candidate__provenance.html": 2905607334694773753, - "docs/api/html/group__PluginConfig.html": 11629742848862930687, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/RECORD": 17341460504914308184, - "docs/api/html/group__SemanticCandidateDto.html": 8345714029489637620, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_api.py": 1910541672533352625, - "venv/lib/python3.13/site-packages/pygments/style.py": 17393199235575070210, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/errors.py": 6538052228921426956, - "backend/src/plugins/translate/orchestrator_lang_stats.py": 5713612003349083382, - "docs/api/html/group__test__code__to__lang__mapping.html": 11085633812653398836, - "backend/src/services/git/_gitea.py": 12239341366900580663, - "docs/api/html/group__test__end__returns__202.html": 3291889015034069048, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/bitwise_ops.py": 14350609278188982939, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta.py": 7742506012012398830, - "venv/lib/python3.13/site-packages/PIL/XVThumbImagePlugin.py": 7472134175570205454, - "docs/api/html/group__ApprovalRequest.html": 11273616744771079528, - "venv/lib/python3.13/site-packages/pygments/lexers/dax.py": 10868881057512002309, - "backend/src/services/clean_release/__tests__/test_preparation_service.py": 1135205964675811608, - "docs/api/html/group__normalizeApiError__Function.html": 11798259322678734628, - "venv/lib/python3.13/site-packages/numpy/_core/_dtype.py": 7796703383405149774, - "backend/alembic/versions/a5b6c7d8e9f0_set_null_ondelete_for_task_records_env_fk.py": 17528129825127496932, - "docs/api/html/group__get__roles__by__ad__groups.html": 5091343705498361491, - "docs/api/html/group__RevokeRequest.html": 3725473471343488816, - "docs/api/html/group__RecommendedAction.html": 7305257791588353178, - "docs/api/html/group__oauth.html": 12697823183291455723, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_dtypes.py": 481191723198819568, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_python_parser_only.py": 12231449578582108346, - "venv/lib/python3.13/site-packages/pyasn1/error.py": 11036037300994516747, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_formats.py": 1265319125235346745, - "venv/lib/python3.13/site-packages/jeepney/bus_messages.py": 5871341372541525842, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_kind.py": 18178830861475480607, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_is_monotonic.py": 4626065848596214108, - "venv/lib/python3.13/site-packages/attr/setters.py": 12467854389745942025, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_formats.py": 2601044064677727807, - "docs/api/html/group__TranslateJobRoutesModule.html": 6272089785836240720, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_array.py": 11271807719833350332, - "venv/lib/python3.13/site-packages/sqlalchemy/event/__init__.py": 16182540084654095394, - "backend/src/api/routes/dashboards/_router.py": 8641007051243802224, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_ssl_constants.py": 14834514243239777933, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_shift.py": 9952689832827833247, - "backend/src/services/clean_release/candidate_service.py": 12771065368619689666, - "venv/lib/python3.13/site-packages/git/objects/submodule/__init__.py": 12958523219496107878, - "backend/src/api/routes/git/_environment_routes.py": 5687847761729712859, - "docs/api/html/group__TaskCleanupModule.html": 3445725949613041336, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/__init__.py": 15130871412783076140, - "frontend/src/routes/settings/MigrationSettings.svelte": 12844990967512238268, - "docs/api/html/group__test__dataset__review__scoped__message__uses__masked__filter__context.html": 17334328430934635110, - "docs/api/html/folderclosedd.svg": 17609606181638232885, - "venv/lib/python3.13/site-packages/starlette/requests.py": 4582424111350387010, - "venv/lib/python3.13/site-packages/numpy/_core/lib/pkgconfig/numpy.pc": 14713682399742251335, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_timedelta.py": 364160935264757405, - "frontend/build/_app/immutable/nodes/24.D-5xQGNl.js": 16241003221424569420, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timestamp.py": 10308799534491190078, - "docs/api/html/group__request.html": 4526564378065461172, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_toml_compat.py": 10196543528439925412, - "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiDelete.html": 3340000838366593929, - "docs/api/html/group__list__users.html": 952281442614367040, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____updateSettings.html": 4714555805820334963, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/endpoint.py": 8519226897299461194, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/status.py": 2214590120238144582, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/datasource.pyi": 16752667877866430577, - "docs/api/html/group__test__resolve__trusted__policy__snapshots__missing__registry.html": 7913805907359428046, - "docs/api/html/search/all_8.js": 12526514117629582170, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_getitem.py": 18202683049446557047, - "docs/api/html/group__test__get__datasets__search.html": 7665831581777860619, - "docs/api/html/group__get__job.html": 11838854381760893535, - "docs/api/html/group__calculate__schedule.html": 15388949849298396012, - "venv/lib/python3.13/site-packages/numpy/ma/testutils.py": 9355402483340137461, - "docs/api/html/group__GlobalSettings.html": 10632007378548267404, - "venv/lib/python3.13/site-packages/cryptography/hazmat/__init__.py": 6534110674636458866, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nditer.py": 8341339612899904236, - "venv/lib/python3.13/site-packages/pandas/tests/apply/conftest.py": 13798335354961261346, - "backend/src/api/routes/dataset_review_pkg/_routes.py": 11040245149296528009, - "docs/api/html/group__save__and__unpack__dashboard.html": 8946899899744210343, - "docs/api/html/group__test__sidebar__initial__state__Function.html": 1960834257863152813, - "docs/api/html/group__AssistantAdminRoutes.html": 8552062072731347537, - "docs/api/html/group__StorageModels.html": 7704378084745051491, - "venv/lib/python3.13/site-packages/pip/_internal/commands/inspect.py": 1401259241736437656, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_symbolic.py": 8093576026032854125, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/conftest.py": 4124267277224879990, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/row.py": 9763935103055608874, - "venv/lib/python3.13/site-packages/passlib/handlers/sha2_crypt.py": 8171587560325812664, - "docs/api/html/group__EncryptionManager.html": 5675366914155455618, - "docs/api/html/group__TraceContextMiddleware__dispatch.html": 11355825183050222114, - "docs/api/html/group__test__init__repository__initializes__and__saves__binding.html": 17804439037629994394, - "venv/lib/python3.13/site-packages/PIL/PcxImagePlugin.py": 1691405132377495336, - "docs/api/html/group__test__belief__scope__logs__reason__reflect__at__debug.html": 16218626126830450612, - "backend/src/core/logger/__tests__/test_logger.py": 392344140757115787, - "docs/api/html/group__resolveEnvUrl__Function.html": 15828348202368551114, - "docs/api/html/group__get__dashboards.html": 12911221475273233338, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_numeric.py": 17391095502314929042, - "venv/lib/python3.13/site-packages/numpy/random/mtrand.cpython-313-x86_64-linux-gnu.so": 16146459667121787202, - "docs/api/html/group__commit__changes.html": 12893499093937665878, - "docs/api/html/group__GitServiceGiteaMixin.html": 16505751774814321714, - "docs/api/html/group__MaintenancePackage.html": 10072412896629514555, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/repr.py": 839417422880281122, - "docs/api/html/group__call__llm__for__batch.html": 16443633855674435189, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_return_integer.py": 16102640589300356639, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_str.py": 149688376805129532, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_explode.py": 16195468192763502449, - "docs/api/html/group__APIKeyUtilities.html": 5445365654414296643, - "docs/api/html/group__error.html": 3228457887146363212, - "venv/lib/python3.13/site-packages/referencing/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__get__run__detail.html": 4564623491331898259, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8693/__init__.py": 11065844267208884531, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arrayprint.pyi": 2053388407376896377, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_value_counts.py": 12340872812120847127, - "docs/api/html/group__derive__recommended__action.html": 17143048057272072667, - "venv/lib/python3.13/site-packages/pandas/_libs/parsers.cpython-313-x86_64-linux-gnu.so": 14134106903573638108, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_function.py": 13492077564015386528, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.APACHE": 11041304845352917971, - "docs/api/html/group__getTask__Function.html": 11413172980831416188, - "docs/api/html/group__openGit__Function.html": 5275859006159727877, - "venv/lib/python3.13/site-packages/websockets/extensions/__init__.py": 13067201313921664210, - "venv/lib/python3.13/site-packages/git/index/typ.py": 11661540784850363024, - "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.py": 4940170059946018356, - "venv/bin/activate": 14735074000123383125, - "venv/lib/python3.13/site-packages/pygments/styles/xcode.py": 14124729477757765388, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft_umath.cpython-313-x86_64-linux-gnu.so": 16443444849116065961, - "venv/lib/python3.13/site-packages/pydantic/v1/error_wrappers.py": 5701679786078971939, - "venv/lib/python3.13/site-packages/packaging/_elffile.py": 9546225990833172494, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_intervalindex.py": 5053113227747333759, - "docs/api/html/group____FakeDb.html": 6191058217277272469, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_astype.py": 15275720655739710249, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/async_app.py": 4329794049222609894, - "venv/lib/python3.13/site-packages/numpy/_distributor_init.pyi": 1308368231471324581, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_swaplevel.py": 3584770792383216626, - "docs/api/html/group__fetchEnvironments__Function.html": 11012013762197915559, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_ewm.py": 13789842684407148830, - "docs/api/html/group__SupersetContextParsingMixin____recover__dataset__binding__from__dashboard.html": 11941775473254198398, - "venv/lib/python3.13/site-packages/cryptography/__init__.py": 641702726372647770, - "docs/api/html/group__JobLifecycle.html": 18086772682144526037, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_ctors.py": 631744056514965940, - "venv/lib/python3.13/site-packages/anyio/_core/_subprocesses.py": 7271614043537119143, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_missing.py": 6224750552277937853, - "docs/api/html/group__PluginLoader.html": 7483612115422215733, - "dist/docker/frontend.0.1.2.tar.xz": 14426231572762407076, - "docs/api/html/search/search.js": 11014809740432474012, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadDashboardBanners.html": 5809931002821855635, - "venv/lib/python3.13/site-packages/ecdsa/der.py": 9374566189581725372, - "venv/lib/python3.13/site-packages/pygments/styles/vim.py": 2621036947954523349, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/resource_owner_password_credentials.py": 9559295461619813890, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_function.py": 14142346897145169685, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____updateResourceTask.html": 10319226416860236365, - "docs/api/html/group__handleSubmit__Function.html": 2642372217347398484, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi": 1626154870044880426, - "docs/api/html/group__ParsedNativeFilters.html": 9224788944366782259, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/random.pyi": 1808392097712315455, - "venv/lib/python3.13/site-packages/pandas/core/indexes/multi.py": 9115751979327393200, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_equals.py": 18261810467289276844, - "venv/lib/python3.13/site-packages/pandas/core/indexes/extension.py": 7193879749426425951, - "docs/api/html/group__LanguageSwitcher.html": 18011055558279343478, - "docs/api/html/group__FeedbackResponse.html": 9253665785027046545, - "venv/lib/python3.13/site-packages/requests/packages.py": 7733041271118298220, - "frontend/src/lib/components/layout/Breadcrumbs.svelte": 8720668241659707399, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/util.py": 10640907133050862176, - "docs/api/html/group__test__authenticate__user.html": 9321693170809204808, - "docs/api/html/group____get__profile__service.html": 16201432222171400696, - "docs/api/html/group__test__very__long__content.html": 5572816377205062437, - "docs/api/html/group__push__span.html": 6501660690027552485, - "docs/api/html/group__docker__frontend__entrypoint.html": 16625957317311694585, - "docs/api/html/group__test__list__runs__empty.html": 2217625981885883059, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_datetime.py": 1678885152993000592, - "venv/lib/python3.13/site-packages/gitdb/typ.py": 16374739906244805047, - "venv/lib/python3.13/site-packages/httpcore/__init__.py": 5897259569277532401, - "frontend/playwright-report/trace/index.html": 6654608714409350530, - "venv/lib/python3.13/site-packages/referencing/jsonschema.py": 14596871117484757502, - "backend/tests/services/clean_release/test_policy_resolution_service.py": 684362936473488668, - "docs/api/html/group__handleDeleteRole__Function.html": 12113537236786108342, - "backend/src/plugins/translate/orchestrator_query.py": 4760681990145471312, - "docs/api/html/group__UtilsModule.html": 2939005577247546143, - "frontend/src/routes/datasets/review/ReviewWorkspaceHeader.svelte": 15302107925819930527, - "docs/api/html/group__run__candidate__register.html": 2561393854945297828, - "docs/api/html/group__update__preview__row.html": 12779817647568588582, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/LCSseq.py": 13853220736499979380, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_unique.py": 16285418015531891530, - "venv/lib/python3.13/site-packages/PIL/ImageFile.py": 15765130658900167517, - "docs/api/html/group__extract__objects__from__zip.html": 4781757022927901861, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/test_pyinstaller.py": 9430762168079129025, - "venv/lib/python3.13/site-packages/starlette/middleware/__init__.py": 9524277524025372856, - "docs/api/html/group__MaintenanceDashboardState.html": 12984094897536307625, - "docs/api/html/group__normalize__task__report.html": 17645268155877338532, - "docs/api/html/group__connect__Function.html": 12882031631631590225, - "docs/api/html/group__test__event__not__found.html": 3590102006915597365, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/authorization_server.py": 4427151727224472650, - "docs/api/html/group__test__get__dialect__from__database__unsupported.html": 1556786983214372955, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_array_ops.py": 6709502157317299163, - "venv/lib/python3.13/site-packages/websockets/legacy/client.py": 4032878587330592478, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/RECORD": 7914594107698271488, - "docs/api/html/group__TestLogger.html": 17549525508288890457, - "venv/lib/python3.13/site-packages/passlib/tests/sample1.cfg": 9597283806285213340, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_diff.py": 2216748778018882333, - "docs/api/html/group__test__manifest__count__mismatch.html": 16626531910432296321, - "docs/api/html/group__batch__detect.html": 7272748616355423819, - "venv/lib/python3.13/site-packages/git/remote.py": 1694919255653049944, - "venv/lib/python3.13/site-packages/pygments/styles/monokai.py": 12381683616720952636, - "venv/lib/python3.13/site-packages/pandas/tests/api/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/_pytest/_io/pprint.py": 11202117031437223527, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pop.py": 14885271225856187795, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_real/foo90.f90": 58572216018544419, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/WHEEL": 691462085217888236, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_aix.h": 13316272821279308986, - "venv/lib/python3.13/site-packages/pytest/__init__.py": 6137272437722350673, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_ewm.py": 12153690217678546798, - "venv/lib/python3.13/site-packages/pandas/compat/__init__.py": 6077099417711209303, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.cpython-313-x86_64-linux-gnu.so": 12473903676666680134, - "venv/lib/python3.13/site-packages/PIL/ImageMorph.py": 13861961228125789866, - "venv/lib/python3.13/site-packages/iniconfig/exceptions.py": 16501270224069836264, - "docs/api/html/group__approve__execution__mapping.html": 11553995581837214740, - "docs/api/html/group__test__get__database__mappings__success.html": 6411080375632795387, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/resources.py": 11533706401259147051, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_indexing.py": 6539365174681852769, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/__init__.py": 15130871412783076140, - "docs/api/html/group__get__config.html": 8145620257276901599, - "docs/api/html/group__TranslateRouterModule.html": 17322360463251819230, - "venv/lib/python3.13/site-packages/pygments/lexers/maxima.py": 11824576541091274641, - "docs/api/html/group__test__delete__job.html": 2874894259644027669, - "venv/lib/python3.13/site-packages/fastapi/_compat/__init__.py": 12380131786677943349, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.pyf": 14358606965464506398, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/typing/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__SupersetClientGetDashboardPermalinkState.html": 14465481522755672093, - "venv/lib/python3.13/site-packages/pandas/tests/io/conftest.py": 5864037534050136837, - "venv/lib/python3.13/site-packages/fastapi/_compat/v1.py": 9776181748061371513, - "docs/api/html/group__test__get__report__detail__not__found.html": 11700510579759808202, - "docs/api/html/group____seed__with__candidate__policy__registry.html": 2918236783591851190, - "venv/lib/python3.13/site-packages/smmap/__init__.py": 4557520096828426601, - "venv/lib/python3.13/site-packages/uvicorn/config.py": 17317823506849880916, - "backend/src/plugins/translate/dictionary_validation.py": 16674754716565921261, - "venv/lib/python3.13/site-packages/pandas/io/formats/format.py": 3364941338174434661, - "docs/api/html/group__ComplianceStages.html": 10094047934285993702, - "docs/api/html/group__TopNavbarStoreTest__Module.html": 11592246663822811031, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_re.py": 1296579393600719224, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/licenses/LICENSE": 8364608327598414179, - "docs/api/html/group__isItemAllowed__Function.html": 1849064607972812978, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_histograms.py": 47631283818191049, - "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.pyi": 84606125837677197, - "venv/lib/python3.13/site-packages/PIL/GifImagePlugin.py": 16073125611077200693, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/events.py": 13473858896579666058, - "docs/api/html/group__CorrectionConflictResult.html": 16510702604708035567, - "venv/lib/python3.13/site-packages/passlib/crypto/__init__.py": 16409196868630049365, - "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.py": 12355826783958402233, - "venv/lib/python3.13/site-packages/passlib/tests/test_context.py": 15195609282700801214, - "docs/api/html/group__test__multimodal__model__returns__true.html": 4867921283780529014, - "docs/api/html/group__test__us3__preview__endpoint__returns__failed__preview__without__false__dashboard__not__found__contract__drift.html": 951503115224623642, - "docs/api/html/group__handleUpdateSchedule__Function.html": 3932290098337453365, - "docs/api/html/group__test__extract__native__filters__from__key.html": 3900857029224500253, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_setops.py": 13440916133402971190, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cython.py": 14695721638531081573, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/base.py": 16891437410462671317, - "backend/src/models/api_key.py": 4327024700622965951, - "backend/git_repos/remote/test-repo.git/objects/aa/ea042a37c6f7bd010d91db93951328da7f01cc": 11936716919619613939, - "venv/lib/python3.13/site-packages/pydantic/_internal/_import_utils.py": 4029157986210295967, - "venv/lib/python3.13/site-packages/pandas/io/xml.py": 326466573926392764, - "backend/src/api/routes/reports.py": 11259252279199691691, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/pg8000.py": 6580512354643849828, - "docs/api/html/group__api__include__routers.html": 15917122012230549418, - "docs/api/html/group__RepositoryRelations.html": 8149399210221155946, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/REQUESTED": 15130871412783076140, - "backend/src/models/dataset_review_pkg/_finding_models.py": 13502025287982574359, - "backend/src/core/scheduler.py": 8479779689131957613, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/METADATA": 14036738037171597049, - "docs/api/html/group__test__compliance__run__executes__as__task__manager__task.html": 3028064195691881216, - "frontend/src/lib/components/translate/__tests__/TargetSchemaHint.test.js": 8659643137288661057, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/WHEEL": 18203500019759199992, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_convert_dtypes.py": 5005259275646154894, - "docs/api/html/group__DEFAULT__LLM__ASSISTANT__SETTINGS.html": 18119385185452388205, - "backend/src/core/auth/oauth.py": 2441694487419087464, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py": 14946223589457772924, - "backend/src/plugins/translate/metrics.py": 11628699537333742678, - "frontend/src/services/toolsService.js": 1568142461283466080, - "venv/lib/python3.13/site-packages/referencing/py.typed": 15130871412783076140, - "docs/api/html/group__TranslateTargetSchemaApi.html": 13948109670112640865, - "docs/api/html/group__handleBulkSync__Function.html": 8500634733610681572, - "venv/lib/python3.13/site-packages/smmap/test/test_mman.py": 8977119225749253844, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/__init__.py": 697601512486720554, - "venv/lib/python3.13/site-packages/urllib3/http2/probe.py": 4069929463671970243, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/tags.py": 254599446317461188, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_array_from_pyobj.py": 9596224861290975079, - "venv/lib/python3.13/site-packages/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py": 14824388798676578409, - "backend/src/plugins/translate/_run_source.py": 11569137727611732019, - "docs/api/html/group__test__get__run__detail__success.html": 7842784758779741648, - "venv/lib/python3.13/site-packages/pydantic/functional_serializers.py": 15285359935059345032, - "docs/api/html/group__MigrationDryRunOrchestratorModule.html": 996754244754452705, - "venv/lib/python3.13/site-packages/pandas/core/arrays/categorical.py": 15993211255162930859, - "docs/api/html/group__test__create__gitea__pull__request__retries__with__remote__host__on__404.html": 16699153216493033230, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timezones.pyi": 17528421279543420867, - "docs/api/html/group__seed__permissions.html": 4126130443403331361, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7515/jws.py": 7505219131537941130, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/__init__.py": 15130871412783076140, - "venv/bin/pyrsa-keygen": 8222542582170363672, - "docs/api/html/group__HealthStore.html": 7646703134391863590, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_common.py": 2223069451949206319, - "docs/api/html/group__DictionaryEntryCreate.html": 6174105871099793072, - "venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py": 10265081145401348863, - "venv/lib/python3.13/site-packages/websockets/extensions/permessage_deflate.py": 12957105912018691390, - "venv/lib/python3.13/site-packages/numpy/core/shape_base.py": 17925759824894973734, - "docs/api/html/group__test__creates__new__banner.html": 4106644046947123822, - "venv/lib/python3.13/site-packages/certifi/__main__.py": 13417658012431779061, - "venv/lib/python3.13/site-packages/starlette/middleware/cors.py": 11384893784314197864, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/binding.py": 17078280153372356796, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/azure.py": 13529763490186254462, - "docs/api/html/group__TestConstantsAuditFixes.html": 1281986242324537213, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/chararray.pyi": 17340224970247832290, - "venv/lib/python3.13/site-packages/pygments/styles/bw.py": 869630132791421268, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/__init__.py": 2246704695838251846, - "venv/lib/python3.13/site-packages/pygments/lexers/srcinfo.py": 16466766917840327610, - "venv/lib/python3.13/site-packages/numpy/matlib.py": 112839038090033740, - "frontend/src/components/storage/FileList.svelte": 6226735853135032532, - "docs/api/html/group__HealthService.html": 2595257407223931546, - "venv/lib/python3.13/site-packages/PIL/_imagingmorph.pyi": 18222325750818585549, - "docs/api/html/group__DatasetMapper.html": 8174296786664861741, - "frontend/build/_app/immutable/chunks/uoY2-oxA.js": 17505982193573114818, - "venv/lib/python3.13/site-packages/websockets/extensions/base.py": 2457233337788783873, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_size.py": 16658280641516916790, - "docs/api/html/group__test__resolve__merge__conflicts__passes__resolution__items__to__service.html": 3503298276532245309, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_semicolon_split.py": 8153770165829451525, - "docs/api/html/group__frontend__src__services__toolsService__js____getTaskStatus.html": 2867392422291554776, - "docs/api/html/group__WebSocketLogHandler__get__recent__logs.html": 5454880085918399099, - "venv/lib/python3.13/site-packages/pydantic/v1/typing.py": 13467331592583800299, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_spss.py": 4865360315909532603, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/containers.py": 5969831151685016772, - "docs/api/html/group__get__repository__status.html": 2242859351176196482, - "docs/api/html/group__test__all__keys__existing__returns__empty.html": 2310303904698481345, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_stata.py": 3030675968330529905, - "docs/api/html/group__test__trigger__run__no__dashboards.html": 16608562786085903819, - "docs/api/html/group__add__task.html": 557592669952894708, - "docs/api/html/group__create__role.html": 7308394036777191380, - "docs/api/html/group____config__manager.html": 8330722751801305600, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_coercion.py": 3551994404797560483, - "venv/lib/python3.13/site-packages/pandas/core/computation/common.py": 5369352684655954608, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_transform.py": 6759417099641034692, - "docs/api/html/group__test__root__path__default.html": 3749097591798015561, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/models.py": 5656065513234782067, - "backend/src/plugins/translate/__tests__/test_inline_correction.py": 9293076489563685553, - "docs/api/html/group__test__backup__dashboards__success.html": 15648044281266776906, - "venv/lib/python3.13/site-packages/jsonschema/tests/fuzz_validate.py": 2328585939822059352, - "venv/lib/python3.13/site-packages/attrs/validators.py": 10346563361083429998, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/jwk.py": 6458001384702613165, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/urllib3/util/request.py": 9432067988603015849, - "frontend/build/_app/immutable/chunks/DgWyDTCI.js": 11883702220404619897, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz.py": 1094797446508902066, - "docs/api/html/group____dataset__review__conflict__http__exception.html": 2685921445603174181, - "docs/api/html/group__parse__dataset__selection.html": 6178748760718774225, - "venv/lib/python3.13/site-packages/websockets/frames.py": 4901850477414613953, - "frontend/e2e/tests/git.e2e.js": 10018194126657448104, - "frontend/src/routes/datasets/review/[id]/__tests__/dataset_review_workspace.ux.test.js": 14106200953934418963, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_records.py": 18167578114159312327, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_constructors.py": 6440526981746306640, - "docs/api/html/group__missing__mandatory__stages.html": 7547931486255029352, - "docs/api/html/group__setToken__Function.html": 17680187825961724392, - "docs/api/html/group__MigrateRequest.html": 12258712854787042018, - "frontend/src/lib/api/translate/jobs.js": 9613844538417291513, - "docs/api/html/group__test__async__network__404__mapping__keeps__non__dashboard__endpoints__generic.html": 15980107135135079303, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.pyi": 5973671421648618563, - "docs/api/html/group__mock__deps.html": 1002676967209626799, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_skew_kurt.py": 15225601437354712351, - "docs/api/html/group____apply__git__identity__from__profile.html": 14258939660260091015, - "docs/api/html/group__rbac__permission__catalog.html": 4836642153723426530, - "docs/api/html/group__test__extract__objects__from__zip__collects__all__types.html": 627526279226989787, - "venv/lib/python3.13/site-packages/apscheduler/triggers/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/websockets/imports.py": 1995356703044158461, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_neighborhood_iterator_imp.h": 10634781863364525895, - "venv/lib/python3.13/site-packages/pandas/_libs/internals.pyi": 11662356720550765508, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_envs.py": 5657151394155576509, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/unistring.py": 18037463865539157041, - "venv/lib/python3.13/site-packages/pandas/_config/__init__.py": 3577026529387563519, - "docs/api/html/group__ReviewWorkspaceLeftSidebar.html": 17632781333650288823, - "docs/api/html/group____seed__candidate__with__passed__report.html": 16678234707349492648, - "docs/api/html/group__plugins__Data.html": 14082713869477656555, - "docs/api/html/group__test__get__task__logs__not__found.html": 3813624608832592211, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE": 17675192170670564526, - "docs/api/html/group__auth__service.html": 15607649240314134855, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/types.py": 12298292195929126306, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_initialstub_already_started.py": 5562030563802006923, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pipe.py": 7400355977900310233, - "docs/api/html/group__SupersetCompilationAdapter__compile__preview.html": 14843229639835525197, - "docs/api/html/group____to__preference__payload.html": 10972305312357222563, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_numerictypes.py": 8836108334624807647, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_value_counts.py": 5534713464063604301, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply.py": 12349190935180702377, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/RECORD": 6518517239543344179, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/WHEEL": 8600534672961461758, - "docs/api/html/group__GitWorkspacePanel.html": 15537793348093112183, - "frontend/build/_app/immutable/chunks/BjGbKstH.js": 14925544056622079547, - "venv/lib/python3.13/site-packages/numpy/_core/overrides.py": 252940520997718714, - "backend/src/api/routes/translate/_preview_routes.py": 12316180738640292691, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_normalize.py": 3203921571999101793, - "docs/api/html/group__test__list__runs__with__data.html": 12537031394595357439, - "docs/api/html/group____get__or__init__publications__store.html": 8202901534868859807, - "docs/api/html/group__MarkerLogger__explore.html": 6168160399723675032, - "venv/lib/python3.13/site-packages/pydantic/v1/errors.py": 11245135449608452720, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_os.h": 7883047506449405400, - "docs/api/html/group__test__case__insensitive__matching.html": 9410338803903481371, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_arithmetic.py": 16045758521544316139, - "venv/lib/python3.13/site-packages/pandas/io/common.py": 12088195203620154171, - "docs/api/html/group__ValidationPolicyResponse.html": 1043672337453196849, - "venv/lib/python3.13/site-packages/pandas/tests/base/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/METADATA": 14543822270526146783, - "venv/lib/python3.13/site-packages/pygments/styles/perldoc.py": 4857683935715743356, - "frontend/src/lib/components/ui/DatasetSearchCombobox.svelte": 14283363077342392531, - "venv/lib/python3.13/site-packages/sqlalchemy/util/topological.py": 11586868018611359415, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/WHEEL": 7454950858448014158, - "backend/tests/scripts/test_clean_release_cli.py": 14028295526033195944, - "docs/api/html/group__test__api__correction__missing__dict.html": 16638106162967496087, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nunique.py": 8450574751331186943, - "venv/lib/python3.13/site-packages/pyasn1/__init__.py": 8089201217593738032, - "venv/lib/python3.13/site-packages/certifi/__init__.py": 9045580998764948967, - "docs/api/html/group__SupersetClientGetDatasetDetail.html": 13570997899949707169, - "venv/lib/python3.13/site-packages/pyasn1/type/namedval.py": 8273171503516887681, - "docs/api/html/group____get__clarification__engine.html": 14952466145960727021, - "docs/api/html/group__MetricItem.html": 9045572594838566942, - "docs/api/html/group__test__cors__parsing__single__value.html": 5377998052395946901, - "venv/lib/python3.13/site-packages/rsa/py.typed": 11041940105507257053, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_masm.asm": 15349921092257597740, - "venv/lib/python3.13/site-packages/pip/_internal/index/__init__.py": 14829074315307489628, - "venv/lib/python3.13/site-packages/PIL/DdsImagePlugin.py": 10646832373516499548, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/dml.py": 17308908394336670576, - "frontend/build/_app/immutable/nodes/0.BIS2DzEe.js": 11017570122564463306, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_writers.py": 103545128256076790, - "backend/src/api/routes/__tests__/test_clean_release_source_policy.py": 15459112324357199814, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/__init__.py": 260774374497653876, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/dh.py": 5728866271812501558, - "docs/api/html/group__test__mutation__endpoints__surface__session__version__conflict__payload.html": 6432996424114233697, - "docs/api/html/group__add__dictionary__entry.html": 15530172718575929284, - "docs/api/html/group__read__users__me.html": 12849277501505045646, - "docs/api/html/group__TranslateRunRoutesModule.html": 10815514676447136360, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/nested_secrets.py": 15691917061715429369, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_handlers.py": 556092537518149789, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/__init__.py": 16123561007855424016, - "venv/lib/python3.13/site-packages/pygments/lexers/jvm.py": 897669925284011089, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/greenlet/TGreenlet.hpp": 6993553444981671786, - "docs/api/html/group____detect__delimiter.html": 10132233034331217270, - "venv/lib/python3.13/site-packages/pandas/core/sparse/__init__.py": 15130871412783076140, - "docs/api/html/group__SupersetChartsMixin.html": 10694368076693575185, - "backend/src/plugins/llm_analysis/__init__.py": 9254812131558249924, - "docs/api/html/group__test__remove__nonexistent__validation__job.html": 7059450961386170424, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsin.csv": 1665171675551692225, - "venv/lib/python3.13/site-packages/yaml/_yaml.cpython-313-x86_64-linux-gnu.so": 16990140871649227895, - "backend/alembic/script.py.mako": 7041141123739810216, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_npfuncs.py": 13987318550267921389, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_filters.py": 17271344092069736999, - "docs/api/html/group__getStatusColor__Function.html": 5211543247156991189, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/decoder.py": 11876648884087997745, - "docs/api/html/group__test__validate__path__read__only__parent.html": 984350365930917404, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.py": 12636954896699095212, - "frontend/src/routes/dashboards/+page.svelte": 5522148689707175338, - "docs/api/html/group__TranslateSchedulerTests.html": 5785296020890210045, - "venv/lib/python3.13/site-packages/pygments/lexers/rust.py": 8605690716125176329, - "backend/src/services/clean_release/manifest_service.py": 7248530010607024664, - "frontend/src/lib/api/assistant.js": 12514226537527897950, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py": 6510864297510700136, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_setops.py": 10550603120090683350, - "backend/src/services/clean_release/compliance_execution_service.py": 11572933296960110695, - "venv/lib/python3.13/site-packages/numpy/f2py/__version__.pyi": 9759534743796867731, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/client_credentials.py": 256154392957334831, - "venv/lib/python3.13/site-packages/pygments/lexers/solidity.py": 5071425960622305629, - "backend/src/plugins/translate/__tests__/test_clickhouse_insert_integration.py": 990102078989024575, - "docs/api/html/group__teardown__class.html": 7748593185053531837, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py": 15130871412783076140, - "frontend/src/components/git/GitReleasePanel.svelte": 11177244258160406730, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayprint.py": 13335063050941743734, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/contains.py": 3098087827310439993, - "docs/api/html/index.html": 6265284219880825892, - "docs/api/html/group__CommitCreate.html": 3838920800459063824, - "venv/lib/python3.13/site-packages/sqlalchemy/util/preloaded.py": 3719702436309355562, - "docs/api/html/group__AuthConfigModule.html": 14846201783459708048, - "docs/api/html/group__get__batches.html": 597462312194719420, - "venv/lib/python3.13/site-packages/certifi/cacert.pem": 14727832973577663826, - "docs/api/html/group__profile__route__deps__fixture.html": 16691986014866091416, - "frontend/src/lib/api/datasetReview.js": 11780670121350173465, - "venv/lib/python3.13/site-packages/rsa/key.py": 4783269051050521475, - "docs/api/html/group__QuestionState.html": 7538640298895031118, - "docs/api/html/group__tasks__engine.html": 7169537698069676064, - "docs/api/html/group__ProfileApiModule.html": 1705516616452689774, - "docs/api/html/group__test__case__insensitive.html": 15150512929501431253, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_utils.py": 15197339054699289627, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/misc.py": 6571241778569465264, - "frontend/src/routes/settings/+page.svelte": 13008569925433466788, - "docs/api/html/group____extract__dataset__review__target.html": 3215287800092748207, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/types.py": 13384984564922916167, - "venv/lib/python3.13/site-packages/_cffi_backend.cpython-313-x86_64-linux-gnu.so": 829323374625147265, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py": 17216492046645992674, - "backend/src/plugins/translate/orchestrator_sql.py": 15420456250044832567, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_constructors.py": 5627819885594450614, - "venv/lib/python3.13/site-packages/numpy/lib/_histograms_impl.pyi": 6534825471013879522, - "venv/lib/python3.13/site-packages/tzlocal/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/RECORD": 8201759541483070364, - "venv/lib/python3.13/site-packages/passlib/handlers/ldap_digests.py": 8658529184965699766, - "venv/lib/python3.13/site-packages/pydantic/_internal/_validators.py": 2939255306814626438, - "docs/api/html/group__SupersetUserProjectionMixin.html": 8962253689270385612, - "docs/api/html/group__getSuggestion__Function.html": 1195438535838483497, - "venv/lib/python3.13/site-packages/urllib3/exceptions.py": 9687187566231423953, - "docs/api/html/group__revoke__publication.html": 13242569991906257496, - "docs/api/html/group__test__revoke__already__revoked.html": 5813717002399486452, - "frontend/src/app.html": 10207534912546549080, - "venv/lib/python3.13/site-packages/pyasn1/compat/integer.py": 7365270971777550573, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_iloc.py": 16160558158155038350, - "docs/api/html/group__frontend__src__lib__stores__health__js____refresh.html": 8625269529469601783, - "docs/api/html/group__test__no__matching__dashboards.html": 4286276093696893337, - "venv/lib/python3.13/site-packages/greenlet/tests/test_gc.py": 6383305344377882524, - "venv/lib/python3.13/site-packages/_pytest/_io/__init__.py": 1510478951261938348, - "docs/api/html/group__list__events.html": 1944931140844300363, - "backend/src/models/__init__.py": 13280325675752905975, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/METADATA": 8985010079096958084, - "docs/api/html/group__frontend__src__services__storageService__js____deleteFile.html": 7628993621903207351, - "venv/lib/python3.13/site-packages/charset_normalizer/cli/__main__.py": 2148876100501691980, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_finfo.py": 17174674209412768422, - "docs/api/html/group____resolve__environment.html": 16547133577275735340, - "venv/lib/python3.13/site-packages/passlib/ifc.py": 14479133654556657428, - "venv/lib/python3.13/site-packages/smmap/buf.py": 18231775424238352734, - "docs/api/html/group__submit__correction.html": 14575546737554094961, - "docs/api/html/group__AsyncSupersetClient.html": 8532694979035205515, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/handlers/cisco.py": 15004707325761459775, - "frontend/public/vite.svg": 13568221685541582385, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapper.py": 16715864918034312591, - "venv/lib/python3.13/site-packages/numpy/lib/_function_base_impl.py": 14034239842770171660, - "docs/api/html/group__test__git__default__repos__path__constant.html": 5012382907811240841, - "docs/api/html/group____discover__route__permissions__cached.html": 18354861251821323391, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/fixed_string.f90": 8654788371538544793, - "venv/lib/python3.13/site-packages/pandas/_libs/sas.cpython-313-x86_64-linux-gnu.so": 16309838137741124637, - "venv/lib/python3.13/site-packages/fastapi/_compat/main.py": 13016492818231995760, - "venv/bin/pyrsa-encrypt": 5858278204727417482, - "docs/api/html/group__CheckStageResult.html": 1868186322282636304, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_shift.py": 8547819448301023303, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/mypy.ini": 4763385838822685881, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nlargest_nsmallest.py": 11049656379959696695, - "venv/lib/python3.13/site-packages/_pytest/timing.py": 14080119171718125986, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_almost_equal.py": 9333355453468475330, - "docs/api/html/group____serialize__empty__clarification__state.html": 4887954579013674680, - "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/highway/LICENSE": 15636145745140922094, - "docs/api/html/group__test__load__schedules__loads__validation__policies.html": 9504795440748068750, - "docs/api/html/group__LaunchDatasetCommand.html": 9231022036284145569, - "docs/api/html/group__MappingsPageTemplate.html": 11838633745376383178, - "docs/api/html/group__StartSessionCommand.html": 15495582448553758086, - "venv/lib/python3.13/site-packages/pygments/formatters/pangomarkup.py": 17135620955205945255, - "docs/api/html/group__EnvironmentContextStore.html": 3222220561881374991, - "frontend/src/routes/tools/backups/+page.svelte": 2765328366215862593, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_round.py": 8336918400205461173, - "venv/lib/python3.13/site-packages/git/refs/reference.py": 2038789918710608481, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/x963kdf.py": 14995426099932276090, - "venv/lib/python3.13/site-packages/pip/_internal/commands/completion.py": 4435231315212798769, - "venv/lib/python3.13/site-packages/pygments/lexers/blueprint.py": 6370363226580199085, - "backend/src/plugins/translate/preview_response_parser.py": 15690759478655959205, - "docs/api/html/group__run__compliance__report.html": 4432157291692948554, - "docs/api/html/group__AsyncAPIClient____handle__network__error.html": 14286771511204835103, - "docs/api/html/group____load__confirmation__from__db.html": 8275222931299853306, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/py.typed": 15130871412783076140, - "docs/api/html/group__MigrationEngineModule.html": 4559630898828635226, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/lexers/gcodelexer.py": 13712494327007684318, - "venv/lib/python3.13/site-packages/pandas/core/tools/__init__.py": 15130871412783076140, - "docs/api/html/group__ConnectionContracts.html": 3059252696925019088, - "docs/api/html/search/all_a.js": 3849984850211567813, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_comparisons.py": 2764694394636775220, - "backend/src/services/clean_release/__tests__/test_manifest_builder.py": 4923613189073397440, - "venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py": 450504439085405418, - "docs/api/html/group__test__list__runs__filters.html": 15556674326842168757, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_insert.py": 11635164392966270621, - "backend/src/api/routes/admin_api_keys.py": 6647178786430565995, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_switch_three_greenlets.py": 10923534235697247269, - "venv/lib/python3.13/site-packages/httpcore/_async/http_proxy.py": 13364803556177997526, - "venv/lib/python3.13/site-packages/pygments/lexers/smv.py": 8253688483040957894, - "venv/lib/python3.13/site-packages/jeepney/io/tests/test_threading.py": 6656552495241857519, - "docs/api/html/group__SupersetAuthCache__set.html": 15942031270979749240, - "docs/api/html/group____limited__user.html": 7899041144502275903, - "venv/lib/python3.13/site-packages/secretstorage/dhcrypto.py": 8860116996905452758, - "docs/api/html/group__continue__merge.html": 1077939235555109294, - "frontend/src/lib/stores/__tests__/test_sidebar.js": 2607679830387722396, - "frontend/build/_app/immutable/nodes/13.D2EVQSML.js": 6256355574695772630, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_autocorr.py": 1092316868398391792, - "venv/lib/python3.13/site-packages/PIL/__init__.py": 10393984345353752724, - "venv/lib/python3.13/site-packages/pandas/core/internals/managers.py": 13547912653290063698, - "docs/api/html/group__policy__repository.html": 8836991613608383932, - "docs/api/html/group__toasts__module__Module.html": 5241674397829197436, - "venv/lib/python3.13/site-packages/h11/_headers.py": 7592730288271984585, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py": 9888795404090331319, - "venv/lib/python3.13/site-packages/_pytest/warning_types.py": 10695097290648045427, - "docs/api/html/group__DatasetsResponse.html": 4984654878449311988, - "venv/lib/python3.13/site-packages/referencing/_core.py": 14744795336121513230, - "docs/api/html/group__import__candidate__artifacts__v2__endpoint.html": 11794097000012858321, - "venv/lib/python3.13/site-packages/pyasn1/codec/der/__init__.py": 15728752901274520502, - "venv/lib/python3.13/site-packages/_pytest/__init__.py": 3769333500910148571, - "docs/api/html/group__imports.html": 8979637474882093794, - "venv/lib/python3.13/site-packages/gitdb/base.py": 7927279446353696534, - "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.pyi": 8879196542984811747, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/shared.py": 1162754506656505196, - "frontend/src/services/storageService.js": 9489915460172592752, - "venv/lib/python3.13/site-packages/PIL/XbmImagePlugin.py": 12256750478268246137, - "docs/api/html/group__DatasetReviewApiTests.html": 17203814141092898592, - "backend/src/services/clean_release/__tests__/test_audit_service.py": 10901982516436251314, - "docs/api/html/group__ValidationTaskListResponse.html": 14158939387498447501, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_duplicate_labels.py": 10663105616336167414, - "docs/api/html/group__generate__insert__sql.html": 10581163190354294492, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/dependency.py": 7300594038182867460, - "venv/lib/python3.13/site-packages/pillow.libs/liblcms2-cc10e42f.so.2.0.17": 4221600618602131476, - "docs/api/html/group____serialize__semantic__field.html": 12631098605309415203, - "docs/api/html/group__FeaturesConfig.html": 10248250923166873006, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_timestamp.py": 12253372102602993034, - "docs/api/html/group__PLUGIN__TO__TASK__TYPE.html": 17348671743390700348, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_hist_box_by.py": 16197297231683144299, - "docs/api/html/group__run__init.html": 4726084133334129935, - "venv/lib/python3.13/site-packages/pygments/lexers/verifpal.py": 9190813166686923928, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/signature.py": 3614545297259098566, - "venv/lib/python3.13/site-packages/pip/_internal/index/collector.py": 7454652626329467900, - "docs/api/html/group__TestComplianceExecutionService.html": 5266217464530796767, - "docs/api/html/group__get__consolidated__settings.html": 18360600092435915493, - "docs/api/html/group__accept__preview__session.html": 5962878640752637722, - "venv/bin/normalizer": 10972303485259566838, - "frontend/src/lib/components/assistant/__tests__/assistant_chat.integration.test.js": 18193433457755419816, - "backend/src/plugins/translate/_run_service.py": 1705461175389858266, - "backend/src/services/dataset_review/__init__.py": 3695054675894579272, - "docs/api/html/group__test__task__matches__dashboard__logic.html": 12048228571630804289, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_groupby_shift_diff.py": 1210657479716368827, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/RECORD": 18294169019244826301, - "backend/src/plugins/translate/_utils.py": 7134223868160185578, - "venv/lib/python3.13/site-packages/PIL/_imagingmath.pyi": 18222325750818585549, - "docs/api/html/group__generate__docs.html": 13997260304920699682, - "docs/api/html/group__TranslateScheduleRoutesModule.html": 6135158019690701715, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.BSD": 16833702494864671773, - "venv/lib/python3.13/site-packages/pandas/core/window/ewm.py": 12143982379243011357, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/test_json.py": 12422315190455428620, - "docs/api/html/group__LogStats.html": 908679196460419136, - "docs/api/html/group__build__launch__blockers.html": 7838341297599392615, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py": 13215932786801020434, - "venv/lib/python3.13/site-packages/pillow.libs/libavif-01e67780.so.16.3.0": 13947441788826399574, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/hist.py": 10928173425078493095, - "venv/lib/python3.13/site-packages/rapidfuzz-3.14.3.dist-info/WHEEL": 2823347510103459191, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler.pyi": 9003794318682878317, - "venv/lib/python3.13/site-packages/git/objects/tree.py": 10239027037919781330, - "docs/api/html/group__test__tui__non__tty__refuses__startup.html": 11290435043577300064, - "venv/bin/pyrsa-verify": 6762739642629904442, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi": 9405122752084210696, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.pyi": 51728851886466199, - "venv/lib/python3.13/site-packages/pandas/_libs/hashing.cpython-313-x86_64-linux-gnu.so": 16124435225170610034, - "venv/lib/python3.13/site-packages/multipart/multipart.py": 26386459953377432, - "docs/api/html/group____serialize__session__detail.html": 616460683247422819, - "docs/api/html/group____load__from__legacy__file.html": 14273299865611176783, - "venv/lib/python3.13/site-packages/pydantic/deprecated/copy_internals.py": 1245428320513062166, - "backend/src/plugins/translate/service_inline_correction.py": 11066393548140180037, - "docs/api/html/group__test__build__dataset__preview__query__context__places__recovered__filters__in__chart__style__form__data.html": 18337321725983567020, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_interpolate.py": 6154072863387383113, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/METADATA": 4975169119201255384, - "venv/lib/python3.13/site-packages/pip/_internal/commands/install.py": 17631777189475433503, - "venv/lib/python3.13/site-packages/bcrypt/py.typed": 15130871412783076140, - "docs/api/html/group__test__start__idempotency.html": 11136159718052819459, - "docs/api/html/group__clean__release__facade.html": 6575150253388246217, - "backend/src/models/report.py": 12105030884508258995, - "docs/api/html/group__RootLayout.html": 12934665320478366323, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_array_utils.py": 14682148205379122638, - "docs/api/html/group__test__list__tasks__pagination.html": 5445789220112902473, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hi77.f": 10617763407280454334, - "docs/api/html/group__SessionCollectionResponse.html": 6802234872096434048, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pct_change.py": 18056235438452125034, - "docs/api/html/group__ApiRoutesModule.html": 7565963372446460595, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_setops.py": 16240008859650640717, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_align.py": 6050248183327934780, - "venv/lib/python3.13/site-packages/PIL/ImageStat.py": 18442709639217312689, - "docs/api/html/group__DatasetReviewSessionTests.html": 5186852478551374970, - "venv/lib/python3.13/site-packages/pandas/core/indexes/interval.py": 17965911911189276907, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/ext.py": 8361831054662096695, - "venv/lib/python3.13/site-packages/_pytest/hookspec.py": 387790426108810884, - "venv/bin/fastapi": 12400009726251862770, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/loading.py": 3460142472461437916, - "venv/lib/python3.13/site-packages/annotated_types/__init__.py": 7367790549779882749, - "venv/lib/python3.13/site-packages/numpy/version.pyi": 5075382895829889457, - "docs/api/html/group__frontend__src__lib__utils__debounce__js____debounce.html": 6554406133377600091, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/RECORD": 6488682447051657004, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_pickle.py": 6232650893735715362, - "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.pyi": 12566839271245057531, - "docs/api/html/search/functions_12.js": 2064688989625290905, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/exceptions.py": 7518613450390473990, - "docs/api/html/group____FakeSessionDel.html": 7361382465314395458, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_clip.py": 13498704233745189154, - "frontend/src/routes/datasets/MetricsTable.svelte": 4644516916752609053, - "venv/lib/python3.13/site-packages/gitdb/pack.py": 7828244200815444166, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setError.html": 16852758973526195279, - "venv/lib/python3.13/site-packages/keyring/testing/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/websockets/legacy/handshake.py": 376095144657994920, - "frontend/src/lib/components/dataset-review/__tests__/us2_semantic_workspace.ux.test.js": 7475597145566385341, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_astype.py": 16700461036658228009, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_common.py": 9446944526144164255, - "docs/api/html/group____load__dataset__review__context.html": 2812784457432782300, - "venv/lib/python3.13/site-packages/fastapi/_compat/v2.py": 15078491685917047095, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_raises.py": 9678418010293693275, - "venv/lib/python3.13/site-packages/cffi/parse_c_type.h": 9889309594462820068, - "docs/api/html/group__DatasetReviewSessionVersionConflictError.html": 6831719684562983772, - "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.pyi": 3111521453255595221, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_array.py": 12514884344165043136, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/__init__.py": 15130871412783076140, - "docs/api/html/group__test__submit__correction__creates__entry.html": 16912801094355529454, - "venv/lib/python3.13/site-packages/anyio/streams/text.py": 12507591237176920223, - "venv/lib/python3.13/site-packages/pygments/lexers/_vim_builtins.py": 6842381248727719900, - "backend/tests/conftest.py": 1710415643410199769, - "docs/api/html/group__setAssistantSeedMessage__Function.html": 13104263220941185944, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/background.py": 2105814165112049750, - "venv/lib/python3.13/site-packages/numpy/tests/test_warnings.py": 3154249157134620649, - "docs/api/html/group__delete__schedule.html": 109693276017801304, - "venv/lib/python3.13/site-packages/pygments/styles/stata_light.py": 16319330015324455277, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/LICENSE": 8177430849046795595, - "venv/lib/python3.13/site-packages/git/refs/head.py": 16271128287606894569, - "docs/api/html/group__list__dashboard__banners.html": 14482370673488154596, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/__init__.py": 18019562641348687547, - "docs/api/html/group__TranslateModels.html": 12305275874224350979, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/var_.py": 12699521091170347311, - "dist/docker/frontend.0.1.1.tar.xz": 14558310887004056908, - "venv/lib/python3.13/site-packages/httpx/_transports/default.py": 8391302827983907910, - "venv/lib/python3.13/site-packages/numpy/_core/_umath_tests.pyi": 4587511199710546030, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__test__assistant__message__request__accepts__dataset__review__session__binding.html": 17773310738913307655, - "docs/api/html/group__build__execution__snapshot.html": 11905153660120769510, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_eng_formatting.py": 14768815444701392013, - "venv/lib/python3.13/site-packages/_pytest/_io/terminalwriter.py": 16852767249156780980, - "docs/api/html/group____wait__for__terminal__task.html": 10847232788702296305, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/util.py": 4890614159013873000, - "docs/api/html/group__handleCheckout__Function.html": 13677007543937370261, - "docs/api/html/group__DebugPlugin.html": 16864972402244728207, - "venv/lib/python3.13/site-packages/annotated_types/test_cases.py": 5123395674810472016, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_grouping.py": 5994909693214299971, - "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/RECORD": 15194367076254806710, - "docs/api/html/group__DashboardHeader.html": 15889628844920899233, - "docs/api/html/group__test__persist__tasks__batch.html": 10046649763450222486, - "docs/api/html/group____compute__source__hash.html": 7685714365612338715, - "docs/api/html/group__save__run__context.html": 10013203780160130788, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bus_messages.py": 4323093138307306385, - "venv/lib/python3.13/site-packages/passlib/hash.py": 5238710192614935875, - "docs/api/html/group__frontend__src__services__adminService__js____getADGroupMappings.html": 9312026240413982380, - "docker/backend.entrypoint.sh": 1034669340368151691, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/live_render.py": 14619617939683800204, - "venv/lib/python3.13/site-packages/pyasn1/debug.py": 6305633719847955310, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_xs.py": 1897144171324167738, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_readlines.py": 15928258358116399067, - "frontend/src/lib/api.js": 13896344878456869253, - "docs/api/html/group__MigrationApi.html": 16067246055200738118, - "docs/api/html/search/all_18.js": 11356236066410231589, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_block_internals.py": 17935662848672401507, - "venv/lib/python3.13/site-packages/pygments/lexers/ruby.py": 6898093948224302592, - "docs/api/html/group__AssistantApiTests.html": 15253113969203355593, - "venv/lib/python3.13/site-packages/passlib/hosts.py": 11171402820797380938, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/result.py": 8306497889916212025, - "docs/api/html/group__test__props__definition.html": 1900076617459161710, - "backend/src/plugins/translate/dictionary_entries.py": 11394786590056973146, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA": 7788873460115135563, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/persistence.py": 17908437937265384125, - "docs/api/html/group__delete__dictionary.html": 8818744411907449226, - "docs/api/html/group__SupersetClientGetDatabasesSummary.html": 15095847088724585809, - "venv/lib/python3.13/site-packages/pygments/styles/vs.py": 4188729978573652106, - "backend/src/api/routes/__init__.py": 4671856475376690434, - "backend/logs/app.log.1": 1282701464293071456, - "docs/api/html/jquery.js": 16180958684071666265, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hmac.py": 18132740541139891824, - "venv/lib/python3.13/site-packages/jeepney/io/asyncio.py": 13239992245695516007, - "frontend/src/routes/git/+page.svelte": 3351712750068549963, - "docs/api/html/group____persist__message.html": 7636295246273294885, - "venv/lib/python3.13/site-packages/fastapi/__init__.py": 11054203163075417954, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein_py.py": 11350239659973489646, - "docs/api/html/group__test__table__based__match.html": 4423679603659278105, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/METADATA": 7214286447912789067, - "docs/api/html/group__test__persist__task__with__logs.html": 3561488143169471878, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/scipy_sparse.py": 2091608752719976909, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_windows.py": 13341168721223961872, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/conftest.py": 12368472226536104037, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64-testset-1.csv": 5708101386018266998, - "venv/lib/python3.13/site-packages/PIL/FliImagePlugin.py": 13364963053197422923, - "docs/api/html/group__test__approve__rejects__blocked__report.html": 4677486143136601773, - "docs/api/html/group__HAS__PERMISSION__PATTERN.html": 3740860741409953841, - "docs/api/html/globals_m.html": 5857181081783132873, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/six.py": 1161259802604920635, - "docs/api/html/group__get__tasks__db.html": 282055494774309931, - "docs/api/html/group__FindingArea.html": 416719964302786471, - "venv/lib/python3.13/site-packages/anyio/_core/_resources.py": 8463932838064724564, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_join.py": 1364886077926604427, - "venv/lib/python3.13/site-packages/tzlocal/windows_tz.py": 11696765495002954868, - "venv/lib/python3.13/site-packages/pandas/core/arrays/timedeltas.py": 7318851108932357510, - "docs/api/html/group__deleteRole__Function.html": 10988783686438944545, - "venv/lib/python3.13/site-packages/gitdb/utils/__init__.py": 15130871412783076140, - "docs/api/html/group__GitServiceBase.html": 3176284003306222212, - "backend/src/services/maintenance/__init__.py": 3167476255502727608, - "venv/lib/python3.13/site-packages/pygments/lexers/zig.py": 470431199354307668, - "docs/api/html/group____build__object__diff.html": 13370696190761975058, - "docs/api/html/group__test__reports__detail__openapi__required__keys.html": 16165604713274880229, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/licenses/LICENSE": 1641332741515411734, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/parse.py": 2427019397934437210, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____stopPolling.html": 7926062796723513466, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_formats.py": 17821648708413143557, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_unary.py": 7128845965976836334, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_interp_fillna.py": 5530041283991655415, - "venv/lib/python3.13/site-packages/h11/_readers.py": 832030916375670301, - "docs/api/html/group__ProfileType.html": 7571730309739466085, - "venv/lib/python3.13/site-packages/passlib/handlers/argon2.py": 835074952952017434, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_repr.py": 5577699046802107144, - "venv/lib/python3.13/site-packages/pycparser/c_parser.py": 3954138286651284327, - "venv/lib/python3.13/site-packages/pygments/styles/tango.py": 12389250878249075539, - "venv/lib/python3.13/site-packages/pygments/lexers/_csound_builtins.py": 14530739818491398298, - "docs/api/html/group__AsyncAPIClient____build__api__url.html": 2646689845586170671, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__init__.py": 5895297076117159245, - "venv/lib/python3.13/site-packages/numpy/core/umath.py": 4476812261188108155, - "docs/api/html/group____get__environment__name__by__id.html": 16940554573354196856, - "docs/api/html/globals_func_d.html": 802653481815391766, - "venv/lib/python3.13/site-packages/_pytest/_py/path.py": 8464061867075119556, - "venv/lib/python3.13/site-packages/pandas/tests/test_nanops.py": 8709105600366913011, - "docs/api/html/group__test__get__remote__id__returns__none__for__missing.html": 14781894002344872627, - "venv/lib/python3.13/site-packages/numpy/_core/strings.py": 3623885046208831106, - "docs/api/html/group__router.html": 4047834241221489998, - "venv/lib/python3.13/site-packages/smmap/util.py": 13276170835044840023, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_errstate.py": 10788459171326750780, - "venv/lib/python3.13/site-packages/numpy/fft/__init__.py": 9343449448737026658, - "docs/api/html/group__test__metrics__empty__job.html": 2409072479669749995, - "docs/api/html/group____resolve__dashboard__id__from__ref__async.html": 5782530713734199781, - "venv/lib/python3.13/site-packages/python_dateutil-2.9.0.post0.dist-info/WHEEL": 5990830714395966792, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ufuncs.pyi": 1808147181379894521, - "docs/api/html/group__get__file__by__path.html": 8633346116618658558, - "docs/api/html/globals_c.html": 11969223700440566246, - "venv/lib/python3.13/site-packages/idna/__init__.py": 6489437464172324468, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/__init__.py": 14571251344510763303, - "docs/api/html/minusd.svg": 1684399045313528301, - "venv/lib/python3.13/site-packages/pandas/tests/window/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_arithmetic.py": 4858019700859362660, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_logical_ops.py": 4438104524064299182, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_index.py": 12015738970771703312, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_comparison.py": 4611207880323503098, - "docs/api/html/group__SemanticSourceStatus.html": 14169028665990248848, - "venv/lib/python3.13/site-packages/numpy/testing/overrides.py": 11819432487830192978, - "docs/api/html/group__test__get__dashboards__page__context__other__disables__profile__default.html": 1375161324945488509, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_index.py": 4908727359687606870, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x86_msvc.h": 2465089365642299646, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/sfc64_np126.pkl.gz": 1630203486813146863, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py": 11657469008721907972, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_append.py": 9644955836191900614, - "docs/api/html/group__SessionStatus.html": 3808958363584408274, - "venv/lib/python3.13/site-packages/ecdsa/ellipticcurve.py": 15624510849346629163, - "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.pyi": 9206355685192026844, - "docs/api/html/group__StatsBar.html": 10845858962596045632, - "docs/api/html/group__test__validation__policy__create__rejects__legacy__string__custom__channels.html": 14661357931736830458, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_encs.py": 9703572751543253904, - "docs/api/html/group__test__tui__initial__render.html": 3293664978123216509, - "docs/api/html/group__test__get__report__success.html": 13133681146386093411, - "frontend/build/_app/immutable/chunks/DuB9BfAK.js": 5895744160798083799, - "venv/lib/python3.13/site-packages/numpy/f2py/__init__.py": 7470157159992827389, - "docs/api/html/group__loadConfigs__Function.html": 6456332498353022877, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_repeat.py": 11360772447747837958, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__test__transform__yaml__replaces__database__uuid.html": 906181308081154952, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_loongarch64_linux.h": 6566098590441297326, - "venv/lib/python3.13/site-packages/sqlalchemy/util/langhelpers.py": 1246582552828542841, - "docs/api/html/group__GitService.html": 5644147165416014917, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_concat.py": 4472503149811617440, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_py.py": 6428078990080243608, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/functions.py": 16567226076808693246, - "venv/lib/python3.13/site-packages/pygments/lexers/hexdump.py": 10665271113239513484, - "venv/lib/python3.13/site-packages/greenlet/tests/test_version.py": 6317733744899804292, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_series.py": 10922287546912569276, - "docs/api/html/group__ReportsApi.html": 8927820650969319266, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_formats.py": 5259100336852479395, - "venv/lib/python3.13/site-packages/yaml/serializer.py": 1783761365703703558, - "backend/src/api/routes/clean_release_v2.py": 7430240321470425220, - "venv/lib/python3.13/site-packages/starlette/formparsers.py": 9019779181100355515, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/REQUESTED": 15130871412783076140, - "backend/src/models/dataset_review.py": 18282368342351080120, - "venv/lib/python3.13/site-packages/pandas/tests/internals/test_api.py": 11217428888457119601, - "frontend/src/components/PasswordPrompt.svelte": 2098675312823151283, - "venv/lib/python3.13/site-packages/numpy/_utils/__init__.py": 7731065084036767848, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexerrors.py": 4434868847540549873, - "venv/lib/python3.13/site-packages/numpy/_core/printoptions.py": 3897105663572772838, - "docs/api/html/group__create__task.html": 5627761289813856584, - "venv/lib/python3.13/site-packages/httpcore/_backends/trio.py": 8394426709857317737, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/core": 10616484054735611337, - "docs/api/html/group__migrate__dashboards.html": 825298586869034371, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_axis.py": 4097534573847151853, - "docs/api/html/group__test__update__schedule.html": 10218439875766465243, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/__init__.py": 3969805865048730851, - "venv/lib/python3.13/site-packages/packaging/specifiers.py": 2800449842990194437, - "docs/api/html/group__TranslationPreview.html": 2562373106089262703, - "venv/lib/python3.13/site-packages/greenlet/platform/__init__.py": 15130871412783076140, - "docs/api/html/group__build__idempotency__key.html": 210393855646189124, - "docs/api/html/group__User.html": 7890454832671400915, - "venv/lib/python3.13/site-packages/requests/hooks.py": 9888227370911151341, - "venv/lib/python3.13/site-packages/apscheduler/events.py": 10748346950729364899, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_freq_attr.py": 17290447066681837965, - "docs/api/html/group__test__cot__json__formatter__output.html": 8496099755020894677, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_hermite.py": 3628609433179387988, - "docs/api/html/group__test__filter__state__model.html": 17888526114492502641, - "venv/lib/python3.13/site-packages/fastapi/exceptions.py": 12665775976182239091, - "venv/lib/python3.13/site-packages/cryptography/hazmat/decrepit/ciphers/__init__.py": 63497920264089252, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_freq_attr.py": 1720704043778272895, - "venv/lib/python3.13/site-packages/ecdsa/ecdsa.py": 9770689913380927623, - "docs/api/html/group__AsyncAPIClient____get__auth__lock.html": 17454231653564423743, - "docs/api/html/group__replacer.html": 8138915867492512587, - "docs/api/html/group__ComplianceStage.html": 17814341327770468741, - "docs/api/html/group__GitRouter.html": 4366531192868731265, - "docs/api/html/group__RunDetailResponse.html": 6291679337051583792, - "venv/lib/python3.13/site-packages/starlette/types.py": 6372850128669191435, - "venv/lib/python3.13/site-packages/greenlet/greenlet_cpython_compat.hpp": 8396413256279803400, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_setitem.py": 10389770446834773837, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_unsupported.py": 9115440166074553860, - "venv/lib/python3.13/site-packages/pycparser/ply/__init__.py": 5040371336745852622, - "venv/lib/python3.13/site-packages/pygments/lexers/actionscript.py": 8829560731160428621, - "docs/api/html/group__test__recover__imported__filters__preserves__unmatched__raw__native__filter__ids.html": 1109218303971466328, - "venv/lib/python3.13/site-packages/pygments/lexers/arrow.py": 6699009974224818979, - "venv/lib/python3.13/site-packages/packaging/licenses/_spdx.py": 16440031845441362300, - "venv/lib/python3.13/site-packages/packaging/__init__.py": 5479277484104486860, - "frontend/playwright-report/data/040cf2c6ccc49a09121675d4b65dd4533d488fe1.webm": 10645096005049255831, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/limited_api_latest.c": 5472633890472031814, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/subrout.pyf": 4660465266594480405, - "docs/api/html/group__frontend__src__services__git__utils__js____normalizeEnvStage.html": 13961356311527011805, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_mixins.py": 12351373879926460719, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_multiindex.py": 13723677252432338937, - "frontend/build/_app/immutable/nodes/18.C7dEsSDZ.js": 17145928165446665640, - "venv/lib/python3.13/site-packages/pygments/lexers/c_like.py": 13004061780700827047, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/emath.pyi": 3718540478840842664, - "docs/api/html/group__TestStorageConfigDefaults.html": 2706421657000125549, - "venv/lib/python3.13/site-packages/pandas/core/ops/docstrings.py": 3721650738518391256, - "venv/lib/python3.13/site-packages/pandas/core/util/__init__.py": 15130871412783076140, - "frontend/src/routes/datasets/review/+page.svelte": 8512810578824138490, - "docs/api/html/group____format__permission__key.html": 15264515637376777742, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-1.csv": 9129730764673472086, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/__init__.py": 13331622784664204066, - "docs/api/html/group__frontend__src__services__git__utils__js____resolvePushProviderLabel.html": 7839804684458326617, - "venv/lib/python3.13/site-packages/pandas/api/__init__.py": 1145864207387918302, - "backend/src/core/logger.py": 11870126816969695787, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/key_set.py": 8485970749081951741, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_first_valid_index.py": 15763400665851423183, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/AUTHORS": 17022185907188244883, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_pop.py": 5700439923973346681, - "venv/lib/python3.13/site-packages/pygments/lexers/sophia.py": 15971500155124949058, - "frontend/src/lib/stores/assistantChat.js": 16716458796178537885, - "venv/lib/python3.13/site-packages/pandas/_libs/sparse.pyi": 12526865400413362140, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/scalars.pyi": 3130685119595923378, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numba.py": 18148155441554555618, - "docs/api/html/group__ReportModels.html": 12439114738308381242, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/shape.pyi": 17197248408326188931, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/common.py": 16314033415206173235, - "docs/api/html/group__SessionEventPayload.html": 17617366798998251944, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.py": 5735043585893007971, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_insert.py": 15536994417338770457, - "venv/lib/python3.13/site-packages/websockets/connection.py": 5168795926684067030, - "venv/lib/python3.13/site-packages/pip/_internal/utils/direct_url_helpers.py": 6075890513530296650, - "venv/lib/python3.13/site-packages/pygments/styles/gruvbox.py": 9489017112545247388, - "docs/api/html/group__estimate__row__tokens.html": 6396881984546758852, - "frontend/src/lib/stores/__tests__/assistantChat.test.js": 11108218971449015168, - "frontend/src/lib/Counter.svelte": 3616557554460116236, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/modules.pyi": 131293606732000587, - "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.pyi": 8232620851756907417, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_sorting.py": 10523740688498427929, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_api.py": 10514120091521589282, - "venv/lib/python3.13/site-packages/pygments/lexers/tal.py": 447505725492547461, - "venv/lib/python3.13/site-packages/PIL/_imagingft.pyi": 1385456134120145584, - "docs/api/html/group__loadLoggingConfig__Function.html": 747812477749540392, - "venv/lib/python3.13/site-packages/pygments/lexers/openscad.py": 13045825115225232396, - "backend/src/plugins/translate/__tests__/test_sql_generator.py": 7990788832878981127, - "venv/lib/python3.13/site-packages/starlette/middleware/base.py": 16850428285382256618, - "docs/api/html/group__TaskLogger.html": 15092573139764984795, - "docs/api/html/group__test__orchestrator__start__session__preserves__partial__recovery.html": 7578527106229875658, - "docs/api/html/group__test__remove__validation__job.html": 9367627137821838796, - "venv/lib/python3.13/site-packages/pandas/core/flags.py": 4690844374637430013, - "docs/api/html/group____run__to__response.html": 4944388567764979533, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_truncate.py": 9623613657871046837, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timedeltas.pyi": 3783371827926657291, - "docs/api/html/group__test__calculate__schedule__zero__window.html": 14555909111709212057, - "venv/lib/python3.13/site-packages/websockets/auth.py": 499195617702591757, - "docs/api/html/nav_f.png": 2704451415722649096, - "venv/lib/python3.13/site-packages/anyio/abc/_streams.py": 8089057973257658396, - "docs/api/html/group__ProviderConfig.html": 14874415699044736742, - "backend/src/plugins/translate/_batch_sizer.py": 13074277127276828485, - "venv/lib/python3.13/site-packages/attr/converters.py": 5179615793507170642, - "venv/lib/python3.13/site-packages/pandas/core/methods/to_dict.py": 4653628436393663485, - "docs/api/html/group__oauth2__scheme.html": 13377767828964490913, - "venv/lib/python3.13/site-packages/pydantic/deprecated/json.py": 17692639396786690144, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_equivalence.py": 17843623343419137458, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_internals.py": 10472508540875131797, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py": 2569618488804767890, - "venv/lib/python3.13/site-packages/pygments/lexers/resource.py": 8057351545636349791, - "venv/lib/python3.13/site-packages/PIL/_imagingmorph.cpython-313-x86_64-linux-gnu.so": 11171810899664471137, - "venv/lib/python3.13/site-packages/pandas/_libs/missing.cpython-313-x86_64-linux-gnu.so": 1990755879067132155, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_randomstate_regression.py": 17425202198134409689, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/WHEEL": 9131187629260560775, - "docs/api/html/search/all_15.js": 13325339458310079443, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/validation": 5398883285871046811, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/errors.py": 14634768861119075506, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tan.csv": 6233924374281534211, - "docs/api/html/group__TaskContext.html": 16944274604472450502, - "backend/src/app.py": 5498748389520927188, - "docs/api/html/group__DashboardProfileOverrideIntegrationTest__Module.html": 4086296085552401797, - "venv/lib/python3.13/site-packages/pyasn1/type/useful.py": 10313387611927801339, - "docs/api/html/group__SettingsPageUxTest__Module.html": 2448306819139198817, - "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_matrix_linalg.py": 2858987911347541177, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_box_unbox.py": 7579587620530953711, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_combine_concat.py": 3201068805128232483, - "frontend/src/lib/components/reports/__tests__/report_detail.integration.test.js": 13812015120538720344, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeTab.html": 15048396624822097099, - "docs/api/html/group__SupersetClientDoImport.html": 8043177973453792978, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.BSD": 3858254215169937333, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_collections.py": 7772437080414835462, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/vectorized.pyi": 15498895254516573071, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/assertions.py": 5556667947376168765, - "venv/lib/python3.13/site-packages/passlib/utils/__init__.py": 11704122205941631503, - "venv/lib/python3.13/site-packages/pandas/core/reshape/tile.py": 17137524194129840833, - "docs/api/html/group__TestDryRunOrchestrator.html": 11981206571409362239, - "venv/lib/python3.13/site-packages/_pytest/setuponly.py": 10959102379908419762, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_value_counts.py": 15633881149834196174, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____getProviderById.html": 6376806405547386425, - "docs/api/html/group__WsLogHandlerModule.html": 1965949101963733420, - "docs/api/html/graph_legend.html": 11133044628946024695, - "docs/api/html/group__CommitSchema.html": 8902723025854017338, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pyi": 14150710436003340771, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_combine_first.py": 3100691245280812910, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_object_arr.py": 4725156416161661681, - "docs/api/html/search/all_9.js": 16141436105417826748, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py": 7906029092844029127, - "docs/api/html/group__CoreContracts.html": 17692523189416853827, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_converters.py": 8869440774594127419, - "venv/lib/python3.13/site-packages/numpy/ma/mrecords.py": 687170905107114309, - "docs/api/html/group__test__remove__nonexistent__translation__job__no__error.html": 5929457228764208486, - "docs/api/html/group__test__persist__task__with__datetime__in__params.html": 11036090690820046897, - "venv/lib/python3.13/site-packages/packaging/_musllinux.py": 2568338440457080365, - "frontend/src/lib/components/reports/__tests__/report_type_profiles.test.js": 8052627851850386760, - "venv/lib/python3.13/site-packages/itsdangerous/__init__.py": 6969210114978365656, - "venv/lib/python3.13/site-packages/httpx/_utils.py": 4836536051177895209, - "frontend/src/routes/dashboards/health/+page.svelte": 11635018493969113169, - "backend/src/core/auth/api_key.py": 15898549010462339946, - "venv/lib/python3.13/site-packages/multipart/__init__.py": 6496114190873890851, - "venv/lib/python3.13/site-packages/pygments/lexers/cplint.py": 13990118342267278047, - "venv/lib/python3.13/site-packages/pandas/_testing/asserters.py": 272063856832951430, - "docs/api/html/group__UiState__Store.html": 9990000106969800534, - "docs/api/html/group__test__unknown__command__returns__needs__clarification.html": 12607016898030176212, - "backend/src/api/routes/git/_router.py": 14821560101452369211, - "venv/lib/python3.13/site-packages/_pytest/mark/expression.py": 1821437025841201912, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/wheel.py": 13979536782410081712, - "docs/api/html/group__frontend__src__services__taskService__js____getTask.html": 17745233273406842327, - "venv/lib/python3.13/site-packages/pydantic/v1/_hypothesis_plugin.py": 6383299780629629080, - "docs/api/html/group__AssistantMessageResponse.html": 16366901716381602745, - "venv/lib/python3.13/site-packages/pandas/compat/pickle_compat.py": 11073483984394766474, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/websockets_impl.py": 15673691977157706075, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/casting.py": 12226803454582253097, - "venv/lib/python3.13/site-packages/pygments/lexers/promql.py": 8929925488296216854, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/METADATA": 11557342107649194148, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema.py": 14454690938977069049, - "venv/lib/python3.13/site-packages/authlib/oidc/registration/claims.py": 4161520263207458028, - "docs/api/html/group__handleResumeMigration__Function.html": 578618105352867634, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_iterator.py": 13869355993837330406, - "venv/lib/python3.13/site-packages/pygments/lexers/floscript.py": 944042220740663491, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.cpython-313-x86_64-linux-gnu.so": 16927263232848238698, - "frontend/build/_app/immutable/chunks/C8K-NfB8.js": 522730246953022577, - "venv/lib/python3.13/site-packages/pygments/lexers/crystal.py": 3579547906957125755, - "venv/lib/python3.13/site-packages/pandas/_libs/tslib.cpython-313-x86_64-linux-gnu.so": 10742615805788605480, - "venv/lib/python3.13/site-packages/pydantic/_internal/_validate_call.py": 4078236486720616395, - "venv/lib/python3.13/site-packages/pandas/tests/test_algos.py": 11860014896449369497, - "docs/api/html/group__test__api__key__auth__invalid.html": 11539496437643571562, - "docs/api/html/group__update__consolidated__settings.html": 10739551165144622798, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_style.py": 6849613206971398029, - "venv/lib/python3.13/site-packages/pandas/core/internals/api.py": 14632030170441237968, - "docs/api/html/group__DatasetReviewRoutes.html": 6463487666472915050, - "docs/api/html/group__TaskDrawerTests.html": 15575873839723255766, - "docs/api/html/group__SupersetClientGetDatabases.html": 14956619577295357507, - "backend/src/services/clean_release/audit_service.py": 13892224612412540989, - "venv/lib/python3.13/site-packages/attr/_cmp.py": 3297594878548179300, - "venv/lib/python3.13/site-packages/passlib/utils/des.py": 2650106381178222229, - "backend/src/api/routes/dashboards/__init__.py": 14480706693953038818, - "venv/lib/python3.13/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE": 14610443802372107702, - "docs/api/html/menudata.js": 1163638699645288191, - "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/RECORD": 3617343778671172258, - "venv/lib/python3.13/site-packages/numpy/lib/_datasource.pyi": 5894423389796456573, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/datetimelike_accumulations.py": 14719894187829729175, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_skew.py": 5276102682005383261, - "venv/lib/python3.13/site-packages/pygments/lexers/rita.py": 8105458520706587972, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_common.py": 10797243274520625867, - "docs/api/html/group__test__get__dashboards__search.html": 3310167705185192506, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/WHEEL": 2357997949040430835, - "docs/api/html/group__test__add__logs__single.html": 2862947647775846235, - "docs/api/html/globals_s.html": 14802100612037968134, - "venv/lib/python3.13/site-packages/numpy/testing/__init__.py": 2831275254313750978, - "venv/lib/python3.13/site-packages/pygments/formatters/svg.py": 11686316844190214147, - "backend/git_repos/remote/test-repo.git/objects/cf/d2dcbd55735b8cedf21e04e64ac627c6f9a458": 10982669277486885525, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_freq_attr.py": 7685756734926749093, - "docs/api/html/group__test__get__task__success.html": 10530613508507125281, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/__init__.py": 2397613800206300456, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py": 6152202474302791232, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_asof.py": 17616601748321325187, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_reduction.py": 2671872764983553906, - "venv/lib/python3.13/site-packages/pandas/api/internals.py": 5188134630996087511, - "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.pyi": 15034917167424979097, - "venv/lib/python3.13/site-packages/pygments/lexers/dotnet.py": 17970884907837213099, - "venv/lib/python3.13/site-packages/pandas/tests/extension/conftest.py": 16886862391161243448, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/logging.py": 2426447364230673649, - "venv/lib/python3.13/site-packages/pandas/core/arrays/base.py": 664515424699746697, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/metadata_editable.py": 12494407279914732338, - "docs/api/html/group__frontend__src__lib__stores__sidebar__js____closeMobile.html": 6283987850786715384, - "venv/lib/python3.13/site-packages/numpy/_pyinstaller/tests/pyinstaller-smoke.py": 13570990531070630950, - "backend/src/services/dataset_review/clarification_pkg/_helpers.py": 16131390986170241319, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_sorting.py": 8944017570338511677, - "docs/api/html/group__test__get__payload__preserves__legacy__sections.html": 10719382727830491309, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatter.py": 16499277113779414101, - "docs/api/html/group__list__tasks.html": 2630490387896738201, - "docs/api/html/group__SupersetClientGetDatasetsSummary.html": 13566132585635326488, - "docs/api/html/group____ensure__auth__users__columns.html": 3871758539262511602, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/blocking.py": 3483388533957772681, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_cpu_features.py": 955759654189150497, - "backend/src/core/task_manager/event_bus.py": 1436794780600195410, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_formats.py": 9738552308554950419, - "venv/lib/python3.13/site-packages/uvicorn/loops/uvloop.py": 5385425357504191161, - "docs/api/html/group__removeToast__Function.html": 7201312866448785982, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rank.py": 5817408252050193538, - "venv/lib/python3.13/site-packages/greenlet/TUserGreenlet.cpp": 16258622236210473101, - "venv/lib/python3.13/site-packages/jose/backends/__init__.py": 16581763658239070112, - "docs/api/html/group__TestMigrationRoutes.html": 12902236514120491768, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/aead.py": 8740476942925346444, - "frontend/src/routes/datasets/__tests__/DatasetPreview.test.js": 12281160027416292128, - "docs/api/html/group__ConfidenceState.html": 10944304052724522934, - "backend/src/core/__tests__/test_config_manager_compat.py": 16202699597948342431, - "venv/lib/python3.13/site-packages/keyring/backend_complete.bash": 16589214602970214174, - "venv/lib/python3.13/site-packages/ecdsa/numbertheory.py": 13538480862642502330, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_decimal.py": 3515468590005709183, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/publicmod.f90": 2174121054736811903, - "docs/api/html/group__test__ensure__encryption__key__prefers__process__environment.html": 9430354217520222764, - "backend/src/plugins/maintenance_banner.py": 13137032570032257498, - "docs/api/html/group__SessionCollaborator.html": 5083895824126893676, - "venv/lib/python3.13/site-packages/pydantic/utils.py": 1533455264689378477, - "backend/src/services/clean_release/repository.py": 2475446738311184132, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_fillna.py": 7244491410004453166, - "venv/lib/python3.13/site-packages/pip/_internal/utils/filetypes.py": 4415340223268174913, - "frontend/build/_app/immutable/chunks/rrOTveCl.js": 6718040498448531057, - "docs/api/html/group__consolidate__archive__folders.html": 775395364374267476, - "docs/api/html/group__test__mapping__service__alignment__with__test__data.html": 6791951592601504164, - "docs/api/html/group__test__compliant__run__validation.html": 7010442946526003143, - "venv/lib/python3.13/site-packages/numpy/exceptions.py": 10981418339663118022, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/METADATA": 3390257212824996989, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_replace.py": 15992095120302643273, - "docs/api/html/group__test__combined__metrics__live__and__snapshot.html": 3514785552121043708, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____resetTranslationRun.html": 7263960980597630853, - "docs/api/html/group__ValidationReport.html": 2160384060642848740, - "docs/api/html/group__test__get__datasets__superset__failure.html": 14758789957406942338, - "venv/lib/python3.13/site-packages/more_itertools/recipes.pyi": 16788810745659802820, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/array.py": 3517673819082574078, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py": 9627328185578406121, - "venv/lib/python3.13/site-packages/rpds_py-0.30.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/segment.py": 2555584120510872966, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_typing.py": 10311989961588249147, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/setup.py": 17086801293303202021, - "docs/api/html/group__MarkerLogger______init____.html": 16103251887807394758, - "venv/lib/python3.13/site-packages/pydantic/_internal/_typing_extra.py": 14843908493613991959, - "docs/api/html/search/groups_a.js": 10318961203603531169, - "docs/api/html/group____latest__approval__for__candidate.html": 8034806470992140564, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/sum_.py": 7062070517544958546, - "venv/lib/python3.13/site-packages/git/index/__init__.py": 7874728461616959441, - "docs/api/html/group__resolve__bound__provider__id.html": 6473196790874356733, - "docs/api/html/group__get__preview__session.html": 12746493050692153788, - "docs/api/html/navtreeindex22.js": 6671112876915377349, - "docs/api/html/group__test__trigger__validation__skips__no__dashboards.html": 14447781900606710025, - "venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py": 7733356903092860495, - "venv/lib/python3.13/site-packages/jeepney/io/trio.py": 11859775321483330469, - "scripts/scan_secrets.sh": 10827095619853567115, - "docs/api/html/group__selectedPlugin__Data.html": 6186646738464235130, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.py": 5260748170237305627, - "docs/api/html/group__subscribe__dataset__events.html": 12095461130299345778, - "docs/api/html/group__ApiKeyListItem.html": 5748587586873326651, - "venv/lib/python3.13/site-packages/pyasn1/type/namedtype.py": 1859990775875190116, - "docs/api/html/group__get__dashboard__detail__async.html": 13075524156311537422, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/pbkdf2.py": 3369246520501764730, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.cpython-313-x86_64-linux-gnu.so": 6726636942028566415, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_align.py": 10284128457194204574, - "venv/lib/python3.13/site-packages/PIL/ImagePath.py": 18323293175183057101, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_index_equal.py": 10617870815989304510, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_arithmetic.py": 15071337112968886738, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/tokens_mixins.py": 9923721909178724437, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/base.py": 16714704323747942110, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_old_base.py": 191359377446257900, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.pyf": 10179237442840865225, - "docs/api/html/group__test__derive__server__url__from__remote__strips__credentials.html": 11245056897250870095, - "docs/api/html/group____extract__columns__from__rows.html": 6576741443241931816, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/sync.py": 6189260178302531376, - "docs/api/html/group____build__no__repo__status__payload.html": 15081425468629205529, - "venv/lib/python3.13/site-packages/yaml/events.py": 745772917931702910, - "venv/lib/python3.13/site-packages/pygments/lexers/elpi.py": 5736689911777977822, - "docs/api/html/group__Permissions__Module.html": 12197326930694600842, - "docs/api/html/group__handleBulkPull__Function.html": 10391716980706820447, - "frontend/src/lib/auth/__tests__/permissions.test.js": 14871965971459735540, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/_typing.py": 17598638528781632158, - "venv/lib/python3.13/site-packages/numpy/f2py/_isocbind.pyi": 5786150108623563787, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/licenses/LICENSE": 12684353321716745791, - "venv/lib/python3.13/site-packages/pydantic/_internal/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/dataclasses.py": 12384956801113860889, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_style.tpl": 3352086677417301397, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/functions.py": 14107377669163193871, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__iotools.py": 6260912730310453037, - "venv/lib/python3.13/site-packages/pandas/tests/construction/test_extract_array.py": 3100523370314150013, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_custom_business_month.py": 1613806538807726074, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ma.pyi": 8186102816080813799, - "backend/src/services/clean_release/exceptions.py": 16765031947674227117, - "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators_v1.py": 16723743964226526155, - "docs/api/html/group__test__resolve__from__dictionary__prefers__exact__match.html": 11830592332306970257, - "venv/lib/python3.13/site-packages/_pytest/monkeypatch.py": 2635774457434545573, - "docs/api/html/group__handleDeploy__Function.html": 16324615477613900974, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__DictionaryManagerModule.html": 13851818928225468760, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/resource_protector.py": 3563910821279222695, - "docs/api/html/group__TestGitApi.html": 384988964900518925, - "docs/api/html/group__save.html": 1333375505490554916, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/signals.py": 10390443713463805090, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/__version__.py": 14138134075118474134, - "backend/src/plugins/translate/__tests__/test_lang_detect.py": 2328877106785068272, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/stride_tricks.pyi": 5844806555773292985, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_clip.py": 8087139801274517318, - "docs/api/html/navtreeindex0.js": 1729443049000642083, - "venv/lib/python3.13/site-packages/pygments/modeline.py": 6194365452131561478, - "frontend/src/routes/reports/llm/[taskId]/report_page.contract.test.js": 4485532466755188385, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/secretstorage/item.py": 15486045386471912693, - "venv/lib/python3.13/site-packages/psycopg2/_range.py": 7184417109068014771, - "venv/lib/python3.13/site-packages/pandas/core/groupby/grouper.py": 18110568057754450953, - "docs/api/html/group__invalidate__timezone__cache.html": 10993258758120512207, - "venv/lib/python3.13/site-packages/pygments/lexers/procfile.py": 3860651022061090738, - "venv/lib/python3.13/site-packages/pygments/lexers/special.py": 4972294202790455038, - "venv/lib/python3.13/site-packages/jsonschema/tests/__init__.py": 15130871412783076140, - "docs/api/html/group__test__ad__group__mapping.html": 3525500635803023761, - "venv/lib/python3.13/site-packages/_pytest/warnings.py": 8633394774569709013, - "venv/lib/python3.13/site-packages/passlib/handlers/sha1_crypt.py": 5752556581809938954, - "venv/lib/python3.13/site-packages/keyring/http.py": 1408024724330078009, - "docs/api/html/group__with__source.html": 10190434660091889570, - "docs/api/html/group____get__latest__clarification__session__or__404.html": 13364115311427816838, - "docs/api/html/group__TestCleanReleaseSourcePolicy.html": 16395844908630771518, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/json.py": 6429651405841289922, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_api.py": 5813060676255093777, - "venv/lib/python3.13/site-packages/pandas/io/formats/console.py": 10370334487585281127, - "backend/src/models/llm.py": 4879429354068794925, - "backend/src/services/dataset_review/clarification_engine.py": 16343602687904467259, - "docs/api/html/group__DatasourceColumnResponse.html": 13651374436200605439, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/array_like.py": 2778905818573625118, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/provision.py": 7362363963464621570, - "docs/api/html/group____resolve__environment__id.html": 18224093667095318191, - "docs/api/html/group__build__manifest__snapshot.html": 4326848185717081568, - "docs/api/html/group__state__mock__Module.html": 17946734577134518363, - "venv/lib/python3.13/site-packages/pydantic/tools.py": 8931184638883701008, - "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.pyi": 17639497083041129225, - "docs/api/html/group__handleSaveRole__Function.html": 5310344647439892257, - "docs/api/html/group__PermissionsModule.html": 6386249265249672269, - "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.py": 652332294655046106, - "backend/src/api/routes/__tests__/test_datasets.py": 5175841745704208723, - "docs/api/html/group__derive__final__status.html": 4319149296078719364, - "venv/lib/python3.13/site-packages/fastapi/applications.py": 4975013401500183420, - "venv/lib/python3.13/site-packages/referencing/_attrs.pyi": 10030665705880994025, - "venv/lib/python3.13/site-packages/numpy/testing/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils.py": 10362977406431569031, - "venv/lib/python3.13/site-packages/pygments/lexers/text.py": 10572727755736597715, - "docs/api/html/group__SupersetContextExtractorPackage.html": 16163237109197150819, - "frontend/src/routes/reports/llm/[taskId]/+page.svelte": 809594987288256163, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_floatingpoint_errors.py": 4301355720042319835, - "docs/api/html/group__estimate__token__budget__for__rows.html": 9229322075799591152, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge.py": 11366935134260232329, - "docs/api/html/group__StoresModule.html": 154732489318359356, - "venv/lib/python3.13/site-packages/pandas/io/feather_format.py": 13242733585787888812, - "docs/api/html/group__unlock__field__semantic.html": 6237915006750814401, - "docs/api/html/group__test__api__key__auth__expired.html": 4953714735118238766, - "venv/lib/python3.13/site-packages/numpy/_array_api_info.pyi": 13283968924732680159, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/_ctypeslib.pyi": 10360538024015064036, - "docs/api/html/group__MaintenanceEndResponse.html": 11532173656825010057, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py": 2652248727360250985, - "docs/api/html/group__buildSidebarSections__Function.html": 528946104052993964, - "venv/lib/python3.13/site-packages/pygments/lexers/_scheme_builtins.py": 5722444920479775453, - "frontend/src/components/StartupEnvironmentWizard.svelte": 17705330033482916904, - "docs/api/html/group__stores__module__Module.html": 5582348367108541718, - "docs/api/html/group__build__parser.html": 9963847217279686225, - "frontend/build/_app/immutable/chunks/CJiE3ZVI.js": 17365948425366590616, - "docs/api/html/group__docker__frontend__Dockerfile.html": 17852951405421150533, - "docs/api/html/group__test__missing__auth__db__url__crashes.html": 1426005486065410823, - "venv/lib/python3.13/site-packages/cryptography/x509/ocsp.py": 17973800752927680404, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/unicode_comment.f90": 11326797716215117266, - "docs/api/html/group__test__generate__docs__success.html": 8836757246703621262, - "docs/api/html/group__score__risks.html": 16643542131846980434, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_records.py": 6252761812228230408, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_map.py": 10769974470610467636, - "docs/api/html/group__test__api__key__auth__environment__scope.html": 5049990905458759758, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/structs.py": 4479429145468072604, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_impl.py": 12056958946481531279, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/WHEEL": 16097436423493754389, - "docs/api/html/search/groups_0.js": 14023012673265007156, - "venv/lib/python3.13/site-packages/pandas/tests/extension/json/__init__.py": 7877061921210840955, - "docs/api/html/group__test__create__admin__is__idempotent__for__existing__user.html": 11387718106316703006, - "venv/lib/python3.13/site-packages/PIL/TiffImagePlugin.py": 6845043550424565106, - "frontend/src/components/MissingMappingModal.svelte": 14222261650100098814, - "venv/lib/python3.13/site-packages/passlib/utils/pbkdf2.py": 15802959653228956404, - "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_version.py": 849232015288488204, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_common_basic.py": 1945891789924713537, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/jwe_zips.py": 5184894018177148648, - "docs/api/html/group__test__trigger__run__success.html": 2545532746329268480, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_series.pyi": 7112486730197067087, - "docs/api/html/group__test__recover__imported__filters__reconciles__raw__native__filter__ids__to__metadata__names.html": 9441026336344613856, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/metadata.py": 7204343250279221076, - "venv/lib/python3.13/site-packages/pydantic/experimental/pipeline.py": 9493054841465227788, - "docs/api/html/globals_r.html": 7591223539833121046, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_groupby.py": 2738671407653333726, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_coercion.py": 3339075591248270762, - "docs/api/html/group__ComplianceViolation.html": 2732715043898056869, - "docs/api/html/group__test__base__dir__constant__replaces__parents.html": 16003639855862888658, - "docs/api/html/search/all_11.js": 4961152569590003385, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/WHEEL": 12146818530001975731, - "venv/lib/python3.13/site-packages/dateutil/parser/__init__.py": 11377431154251655738, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/cli.py": 6382193953727339271, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/index_tricks.pyi": 6035507586990711471, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/serialize.py": 11139432164346742421, - "backend/git_repos/remote/test-repo.git/objects/52/ec4a78c629329b8b572ec9d5205271c111baac": 18054296544295201561, - "frontend/src/lib/components/translate/TranslationRunResult.svelte": 4434171203266080686, - "venv/lib/python3.13/site-packages/pygments/lexers/hdl.py": 6772823608124143123, - "docs/api/html/group__SupersetContextExtractorBase____extract__numeric__identifier.html": 8466073084601965618, - "docs/api/html/group__test__missing__variables.html": 14137199728172789432, - "docs/api/html/group__deleteFile__Function.html": 12741403507383620880, - "backend/src/services/clean_release/repositories/approval_repository.py": 11060626515832882475, - "venv/lib/python3.13/site-packages/numpy/_core/strings.pyi": 1981676808027146409, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/cython/meson.build": 9642189398934130247, - "docs/api/html/group__LlmRoutes.html": 16633447644830159048, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_chunksize.py": 7107619871245389835, - "docs/api/html/group__ArtifactType.html": 5232079852731603995, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/char.f90": 12203871998988759364, - "frontend/src/routes/admin/settings/+page.svelte": 9726389419291634649, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/misc/extended_precision.pyi": 6541258584648209609, - "docs/api/html/group__test__build__detector.html": 11732087775356139179, - "docs/api/html/search/all_0.js": 14023012673265007156, - "venv/lib/python3.13/site-packages/greenlet/tests/test_throw.py": 4119265751381210399, - "backend/alembic.ini": 8897071018956157084, - "venv/lib/python3.13/site-packages/python_multipart/decoders.py": 14252672585219598429, - "venv/lib/python3.13/site-packages/pygments/lexers/urbi.py": 1611762607688882372, - "frontend/playwright-report/trace/snapshot.v8KI4P3m.js": 7854911091722310751, - "docs/api/html/group__AuthenticationError.html": 1954448943641493275, - "venv/lib/python3.13/site-packages/PIL/FpxImagePlugin.py": 18245056799205041866, - "docs/api/html/group__PolicyResolutionService.html": 794248469337141330, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Jaro.pyi": 3145980717589050713, - "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.pyi": 15848834171197104301, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/conftest.py": 3691712173202219606, - "venv/lib/python3.13/site-packages/pygments/lexers/json5.py": 9736695186548899403, - "venv/lib/python3.13/site-packages/pygments/cmdline.py": 15628499088643246809, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_values.py": 430620320423334461, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/pyodbc.py": 4146960201984489604, - "venv/lib/python3.13/site-packages/rpds/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE": 4123064337800029995, - "docs/api/html/group__test__repo__path__default__fixed.html": 5743804779893858892, - "docs/api/html/group__bulk__find__replace.html": 7341744188539176239, - "frontend/e2e/tests/login.e2e.js": 9217667783118672119, - "venv/lib/python3.13/site-packages/websockets/streams.py": 15786581670248099415, - "venv/lib/python3.13/site-packages/httpcore/_async/__init__.py": 491192949420297153, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/warnings_and_errors.py": 11172336980866895356, - "backend/src/services/clean_release/stages/no_external_endpoints.py": 5083253025896929417, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/yaml.py": 17801117405950663611, - "docs/api/html/globals_func_h.html": 17098238941487002581, - "venv/lib/python3.13/site-packages/pandas/_libs/window/indexers.pyi": 9911899303567570646, - "backend/test_pat_api.py": 4206656943734821967, - "docs/api/html/group__submit__bulk__corrections.html": 17842098461150569742, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/RECORD": 2024211292009381421, - "venv/lib/python3.13/site-packages/httpcore/_backends/sync.py": 576247220834179928, - "docs/api/html/group__enable__schedule.html": 10104484743481845781, - "docs/api/html/group__test__trigger__validation__skips__inactive__policy.html": 10751741691328808103, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py": 7019253290755243568, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.c": 9134176670140958459, - "docs/api/html/folderclosed.svg": 9648391834591542818, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/emoji.py": 9166774203918731896, - "docs/api/html/group__PreviewSessionManager.html": 4001497686990395756, - "venv/lib/python3.13/site-packages/numpy/_core/_string_helpers.pyi": 12043292763005874815, - "docs/api/html/group__APIKey.html": 15496875141447468476, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_missing.py": 2976310411820303374, - "docs/api/html/group__InitAuthDbScript.html": 8842607924154924200, - "venv/lib/python3.13/site-packages/pandas/core/ops/mask_ops.py": 16152832446926420035, - "venv/bin/numpy-config": 17104619511899110252, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np126.pkl.gz": 9677994062701347206, - "docs/api/html/group__BeliefFormatter.html": 1872767394370220168, - "docs/api/html/group__frontend__src__services__git__utils__js____extractUnfinishedMergeContext.html": 6678834570303525459, - "docs/api/html/group__docker__backend__entrypoint__bootstrap__admin.html": 4653794695033998401, - "docs/api/html/group__generate__api__key.html": 2424917664725615867, - "venv/lib/python3.13/site-packages/numpy/core/_dtype.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_odswriter.py": 5134385709728943904, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_numpy.py": 9663734591667845940, - "venv/lib/python3.13/site-packages/authlib/integrations/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/__init__.py": 7316739139609490520, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/scanner.py": 5640381644804445932, - "docs/api/html/globals_n.html": 4026120167261781447, - "venv/lib/python3.13/site-packages/jose/jwk.py": 2781217726458801910, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_datetimelike.py": 5317270578347271174, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/dtypes.pyi": 15114028503984522176, - "docs/api/html/group__get__maintenance__settings.html": 68073512936304627, - "venv/lib/python3.13/site-packages/apscheduler/executors/pool.py": 10344126569001032241, - "docs/api/html/group__PageContracts.html": 8602909438030144707, - "venv/lib/python3.13/site-packages/PIL/TiffTags.py": 8688883943071546469, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/replace.py": 1708865864686705833, - "frontend/src/routes/tools/debug/+page.svelte": 15220194081518936566, - "docs/api/html/group__SemanticSourceResolver.html": 9852035711802845664, - "docs/api/html/group__test__test__git__config__validates__connection__successfully.html": 7885701889149733525, - "venv/lib/python3.13/site-packages/pandas/errors/cow.py": 14381282473643980776, - "venv/lib/python3.13/site-packages/numpy/random/_philox.pyi": 7503018432880404291, - "venv/lib/python3.13/site-packages/numpy/f2py/crackfortran.py": 12796890117561096360, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/exc.py": 1448374203226983757, - "docs/api/html/group__test__backup__dashboards__with__schedule.html": 11599542107970767027, - "venv/lib/python3.13/site-packages/websockets/asyncio/compatibility.py": 9716993942839407921, - "venv/lib/python3.13/site-packages/cffi/lock.py": 13507686939203117974, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_pickle.py": 16196792378104396231, - "venv/lib/python3.13/site-packages/pygments/lexers/typst.py": 8883289726601667104, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS": 2987771679926243782, - "venv/lib/python3.13/site-packages/keyring/cli.py": 10996746519742056101, - "docs/api/html/group__DatasetReviewOrchestrator.html": 7010827725071537587, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/__init__.py": 14481526616887213120, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_pickle.py": 388513670243918273, - "docs/api/html/group__test__load__tasks__with__limit.html": 16300281296351269896, - "frontend/build/_app/immutable/chunks/CrLU7zQO.js": 14265962769345069959, - "venv/lib/python3.13/site-packages/numpy/_core/_simd.cpython-313-x86_64-linux-gnu.so": 12609666221355091416, - "docs/api/html/group____ensure__dictionary__entries__columns.html": 2292525621373712406, - "docs/api/html/group__params.html": 10578394165537584451, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_inference.py": 16402427186697071465, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/__init__.py": 12975130533860125272, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_latex.py": 15077617224435782028, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_structures.py": 16375837477294135345, - "venv/lib/python3.13/site-packages/fastapi/security/oauth2.py": 1925394177931547776, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/api.py": 13134394673408254097, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/test_constructors.py": 9478848417896027917, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.pyi": 16164841755339571545, - "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_sas.py": 9446226619493404132, - "frontend/src/lib/stores/__tests__/mocks/stores.js": 12764819706469057535, - "docs/api/html/group__calculate__crc32.html": 15503083199679692336, - "docs/api/html/group__insert__batch__to__target.html": 389711692439722572, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_misc.py": 9190561773853901527, - "docs/api/html/group__GitSettingsPageUxTest__Module.html": 7822522085605911446, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/_spdx.py": 17182188548503720124, - "backend/src/schemas/settings.py": 18238532620797946210, - "venv/lib/python3.13/site-packages/anyio/to_interpreter.py": 8369454842687342401, - "docs/api/html/group____seed__candidate__and__passed__report.html": 14818107129922209880, - "backend/src/plugins/translate/__tests__/test_scheduler.py": 5715885061128238668, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_join.py": 13115782559805895662, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dim2.py": 5767600151459095125, - "backend/git_repos/remote/test-repo.git/info/exclude": 6403833611826357791, - "docs/api/html/group__get__logging__config.html": 13172275413110444262, - "venv/lib/python3.13/site-packages/pip/_internal/cli/autocompletion.py": 7651919329635563739, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_conversion.py": 7507083194555424923, - "docs/api/html/group____async__confirmation__summary.html": 4513234989932474240, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/service_utils.py": 15103456860964805370, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/__init__.py": 15130871412783076140, - "backend/tests/scripts/test_clean_release_tui_v2.py": 5872535102885848729, - "docs/api/html/group__ArtifactFormat.html": 11975137496264852869, - "docs/api/html/group__test__create__session.html": 9252520681669959841, - "venv/lib/python3.13/site-packages/numpy/_core/umath.pyi": 11775804030935134661, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/__init__.py": 2345997696115514124, - "docs/api/html/group__frontend__src__types__backup__ts____Backup.html": 5424115272360107965, - "venv/lib/python3.13/site-packages/jose/backends/ecdsa_backend.py": 2684176977865283060, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_strings.py": 7168544888340296373, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/mapped_collection.py": 11377280960650304531, - "backend/src/api/routes/translate/__init__.py": 6357999798616465776, - "docs/api/html/group__TranslationRunRetryManager.html": 16122389205545947641, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/fixtures/orm.py": 7554417559838785150, - "docs/api/html/group__ErrorPage.html": 8821459762904826623, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getRecommendedActionLabel.html": 4071530290797057104, - "venv/lib/python3.13/site-packages/click/shell_completion.py": 5858110451169927373, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_loc.py": 2127584756555503873, - "frontend/build/_app/immutable/chunks/DA0oX2nF.js": 4762485859359244720, - "docs/api/html/search/functions_a.js": 12926479784540505760, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_series.py": 16418555817529711079, - "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/RECORD": 3649834117451110628, - "docs/api/html/group__test__bulk__corrections__atomic.html": 16703462863207899908, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/jwt.py": 9642615960197748105, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_loc.py": 17839380711970027363, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_f2cmap.py": 5863341586534531594, - "venv/lib/python3.13/site-packages/sqlalchemy/events.py": 9329355486544822483, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_liboffsets.py": 14705822637347212540, - "backend/src/plugins/translate/dictionary_correction.py": 18051915404774774954, - "run.sh": 16549488891429944178, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/models.py": 9513172366009392193, - "docs/api/html/group__resetEnvForm__Function.html": 11629856738911116007, - "backend/src/api/routes/assistant/_llm_planner_intent.py": 1835887541250815399, - "venv/lib/python3.13/site-packages/PIL/MpoImagePlugin.py": 644327650732339301, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/client.py": 18443944554871937480, - "docs/api/html/group__SupersetClientBuildDatasetPreviewLegacyFormData.html": 16410404940636182538, - "venv/lib/python3.13/site-packages/fastapi/responses.py": 6092572095549573593, - "venv/lib/python3.13/site-packages/pandas/util/__init__.py": 4070574585004336241, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_interval.py": 2613503970235591726, - "venv/lib/python3.13/site-packages/PIL/GdImageFile.py": 16875784236394545058, - "docs/api/html/group__setupTests__Module.html": 4447681325358958988, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/routines/funcfortranname.f": 9330535808093475892, - "frontend/e2e/helpers/api.helper.js": 12740720187461301874, - "docs/api/html/group__test__patch__profile__preferences__validation__error.html": 8272861585949821764, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_timeseries_window.py": 1973365560758236222, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____resolveEnvStage.html": 14282337486295968298, - "docs/api/html/group____plan__dataset__review__intent.html": 16025749447796355247, - "docs/api/html/group__test__report__validation.html": 17827285092454190901, - "docs/api/html/group__test__cli__manifest__build__scaffold.html": 12976660891449415796, - "docs/api/html/group____normalize__owner__display__token.html": 12722166268029895042, - "docs/api/html/group__orchestrator__sql__rows.html": 11564020210274340689, - "venv/lib/python3.13/site-packages/starlette/staticfiles.py": 11256974306202558529, - "venv/lib/python3.13/site-packages/pandas/core/ops/missing.py": 9493665944764255854, - "docs/api/html/group__SupersetDatasetsPreviewFiltersMixin.html": 6453705945278224985, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/METADATA": 5628172948035858892, - "venv/lib/python3.13/site-packages/pygments/lexers/mips.py": 2078929831513352031, - "docs/api/html/group__ResourceService.html": 11914817317990021619, - "venv/lib/python3.13/site-packages/ecdsa/_sha3.py": 10353612776869489378, - "docs/api/html/group____handle__unexpected__git__route__error.html": 4213682782631891407, - "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.py": 12251264586530536622, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_reductions.py": 3227705565578016389, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_infer_objects.py": 16868563755487479303, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/__init__.py": 15130871412783076140, - "docs/api/html/group__UseReviewSession.html": 15548286754237500808, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_isoc.py": 4923640657296076239, - "frontend/src/routes/migration/mappings/+page.svelte": 13415361018394803129, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__TranslationUtils.html": 10386648824429307238, - "docs/api/html/group__TestDictionaryFilter.html": 24770183405155834, - "docs/api/html/group__test__calculate__schedule__even__distribution.html": 7169536723006571370, - "docs/api/html/group__TestPublicationService.html": 8868260289891747694, - "docs/api/html/group__get__current__user.html": 13871827060442489554, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/RECORD": 17594742868855466393, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_range.py": 3544406211185340973, - "venv/lib/python3.13/site-packages/pygments/lexers/_cl_builtins.py": 16540712925368297957, - "venv/lib/python3.13/site-packages/websockets/sync/connection.py": 2791040669094103200, - "venv/lib/python3.13/site-packages/yaml/tokens.py": 2279161238859232338, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_bin_groupby.py": 14831211659124136806, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_gcc.h": 3561524604274567339, - "venv/lib/python3.13/site-packages/yaml/resolver.py": 6669674521776449098, - "docs/api/html/group____is__valid__runtime__api__key.html": 911899575469202802, - "docs/api/html/search/all_7.js": 449094548201224648, - "venv/lib/python3.13/site-packages/pandas/tests/window/moments/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/__tests__/test_token_budget.py": 2275341408164077909, - "venv/lib/python3.13/site-packages/jeepney/io/tests/utils.py": 10346859893683522113, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/constructors.py": 7587476800569414769, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/printing.py": 8681519649669408795, - "venv/lib/python3.13/site-packages/pandas/io/pytables.py": 17482934215513855928, - "docs/api/html/group__TranslateJobsApi.html": 7339024353697591080, - "docs/api/html/group__PreparePreviewResult.html": 13180386632469700647, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/markup.py": 5413800947725916538, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.pxd": 9126772093305708921, - "venv/lib/python3.13/site-packages/ecdsa/test_ellipticcurve.py": 11125783981574901613, - "venv/lib/python3.13/site-packages/pandas/core/interchange/from_dataframe.py": 8278998381856028462, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_can_hold_element.py": 5406431247116557833, - "venv/lib/python3.13/site-packages/greenlet/greenlet_msvc_compat.hpp": 12504008513945919250, - "docs/api/html/group__test__get__datasets__not__found.html": 12470922355054560217, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/gh_22819.pyf": 14717748358749934995, - "venv/lib/python3.13/site-packages/_pytest/logging.py": 10972553724755511468, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_bar.py": 7791068963246005463, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__StatsCounts.html": 11606389860887165837, - "docs/api/html/group__test__get__run__detail.html": 4912801148491147951, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth1_client.py": 3129955464287331242, - "docs/api/html/group__test__json__load__if__needed__list.html": 793875230537315255, - "docs/api/html/group__ColumnsTable.html": 8262379607630613277, - "docs/api/html/group__startMigration__Function.html": 2057002933686227334, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_indexing.py": 1947969642791009285, - "venv/lib/python3.13/site-packages/greenlet/tests/test_leaks.py": 1219835696230215605, - "venv/lib/python3.13/site-packages/urllib3/util/retry.py": 17988032431204662756, - "frontend/playwright-report/trace/snapshot.html": 11716473056415565262, - "backend/git_repos/remote/test-repo.git/objects/93/9cc583a18267406014c57617ccb557569f6aaf": 5474754694741999746, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/setitem.py": 9552868454186447925, - "frontend/playwright-report/trace/uiMode.html": 2646979321132705707, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA.py": 17817123894903120316, - "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_dt_accessor.py": 2229616362327032253, - "docs/api/html/group__test__get__logs__by__task__id.html": 3774988735198312173, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/io.py": 11072022722163954973, - "docs/api/html/group__ReportsApiTest__Module.html": 14793467354050182333, - "venv/lib/python3.13/site-packages/pandas/_libs/_cyutility.cpython-313-x86_64-linux-gnu.so": 3372660143512868174, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_ints.py": 5616705208267050659, - "frontend/src/components/auth/ProtectedRoute.svelte": 9152870722988000278, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_duplicated.py": 6659959750254523427, - "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.pyi": 6286629088867436848, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_extract.py": 14547819021277949280, - "venv/lib/python3.13/site-packages/pygments/lexers/sas.py": 6128008194712914874, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nlargest.py": 14518132768753249360, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py": 1422768761337195135, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_index_col.py": 17604191644943383567, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/content": 16257335642847993866, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_version.py": 15336513445621042105, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/interfaces.py": 4890958419958257958, - "docs/api/html/group__test__end__active__event__single__banner.html": 329198841271298860, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nested_sequence.pyi": 10985533017688918283, - "venv/lib/python3.13/site-packages/pydantic/_internal/_config.py": 12760390602912369447, - "venv/lib/python3.13/site-packages/pygments/lexers/php.py": 17636450950688953288, - "venv/lib/python3.13/site-packages/attr/exceptions.py": 11532465891810816417, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildAssistantSeedPrompt.html": 4030604807548245084, - "docs/api/html/group__validate__update__payload.html": 17271010803443567335, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/pkg_resources.py": 14271077832999370068, - "venv/lib/python3.13/site-packages/numpy/__init__.py": 2331213407497901634, - "docs/api/html/group__test__trigger__validation__creates__tasks.html": 10655556421929325211, - "venv/lib/python3.13/site-packages/attr/_cmp.pyi": 14443711863388606665, - "venv/lib/python3.13/site-packages/pip/_internal/models/candidate.py": 3099785363867247334, - "docs/api/html/resize.js": 16477870345255205251, - "venv/lib/python3.13/site-packages/pydantic/types.py": 785880062468926292, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_stack_unstack.py": 3737891757817696405, - "venv/lib/python3.13/site-packages/python_multipart/multipart.py": 5677735019292537174, - "venv/lib/python3.13/site-packages/passlib/handlers/postgres.py": 14162382430051693313, - "venv/lib/python3.13/site-packages/passlib/handlers/oracle.py": 12959175637015399815, - "venv/lib/python3.13/site-packages/pip/_internal/models/selection_prefs.py": 14700113736620314923, - "venv/lib/python3.13/site-packages/pydantic_settings/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pyasn1/type/constraint.py": 16776615503418773283, - "frontend/build/_app/immutable/chunks/KxePT7Ip.js": 8106111085962356816, - "docs/api/html/group__test__list__events.html": 8621900407061858699, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_compression.py": 3167245427092658522, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/visitors.py": 5190923415390076080, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_add_prefix_suffix.py": 4815518357936092237, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine_first.py": 11425059090050867171, - "docs/api/html/group__test__reject__persists__decision__without__promoting__candidate__state.html": 7671132833499886533, - "docs/api/html/group__remove__banner__from__position.html": 10490485317940098773, - "docs/api/html/group__test__default__config__root__path.html": 13958925806070579987, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/REQUESTED": 15130871412783076140, - "frontend/src/lib/components/reports/ReportCard.svelte": 7009840605870276729, - "backend/src/plugins/translate/preview_resolve_provider.py": 7349483761713339246, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/METADATA": 3618776395694990076, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_overlaps.py": 2328652590379935311, - "docs/api/html/group__SupersetContextExtractorBase.html": 14341165499851987732, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/auth.py": 13542332344403032059, - "docs/api/html/group__ReportPageContractTest__Module.html": 10375296767582246595, - "venv/lib/python3.13/site-packages/ecdsa/rfc6979.py": 11564966030899162026, - "venv/lib/python3.13/site-packages/numpy/testing/print_coercion_tables.py": 11758491058846790938, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/__init__.py": 15130871412783076140, - "docs/api/html/group__test__parse__superset__link__dashboard__slug__recovery.html": 11791096107070839073, - "venv/lib/python3.13/site-packages/authlib/oidc/core/models.py": 1370998944213578392, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/bootstrap.py": 3865947239497500815, - "venv/lib/python3.13/site-packages/gitdb/db/mem.py": 15823812455996199993, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__SupersetClientGetDashboards.html": 13070946675530774203, - "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.py": 13115032674230220791, - "docs/api/html/group__prepare__candidate__endpoint.html": 8691475225668238751, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/pcg64dxsm-testset-2.csv": 16611724551361692999, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/mod_derived_types.f90": 2556756676983488922, - "docs/api/html/group____EXT__frontend__AssistantChatTest__s.html": 10743878313235782900, - "venv/lib/python3.13/site-packages/pygments/lexers/factor.py": 15788613154609197002, - "venv/lib/python3.13/site-packages/pandas/core/indexes/frozen.py": 1419685664276640120, - "docs/api/html/group__NetworkError.html": 8895892229568746717, - "docs/api/html/group__clearTasks__Function.html": 13599249766000880879, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/syntax.py": 18070449217682341495, - "docs/api/html/group__DatasetReviewModels.html": 11351633006564729227, - "venv/lib/python3.13/site-packages/pandas/tseries/__init__.py": 2648641118117738895, - "docs/api/html/group__CreateComplianceRunRequest.html": 8446298024144622339, - "docs/api/html/group__job__to__response.html": 14491060891738225447, - "venv/lib/python3.13/site-packages/smmap/test/test_tutorial.py": 16309370344201290582, - "docs/api/html/group__serve__spa.html": 411901662117406287, - "venv/lib/python3.13/site-packages/numpy/_core/records.py": 11414828908088050950, - "docs/api/html/group__CHARS__PER__TOKEN__MIXED.html": 1704290881184281632, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_algos.py": 5356470832688354593, - "venv/lib/python3.13/site-packages/keyring/compat/py312.py": 17581691618899404491, - "docs/api/html/group__resolveStorageQueryFromPath__Function.html": 13600414603292373532, - "venv/lib/python3.13/site-packages/pygments/styles/rainbow_dash.py": 12236746494379891602, - "venv/lib/python3.13/site-packages/pandas/util/version/__init__.py": 3844072588696074141, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.cpython-313-x86_64-linux-gnu.so": 2574035311375507054, - "docs/api/html/group__test__get__reports__handles__mixed__naive__and__aware__datetimes.html": 10017276203058438772, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_casting_unittests.py": 18157503533859249155, - "docs/api/html/group__logger.html": 15165757884259946522, - "backend/src/services/__tests__/test_rbac_permission_catalog.py": 2963458864311994307, - "venv/lib/python3.13/site-packages/ecdsa/test_keys.py": 17117188373419689467, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_timedelta.py": 17052461094717926538, - "backend/tests/test_translate_scheduler.py": 9870953183282058719, - "docs/api/html/group__test__parsed__native__filters__model.html": 11879713945172896956, - "docs/api/html/group__load__tasks.html": 13194384238466330387, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/base_key.py": 10402800587070352851, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/themes.py": 6451091097727599550, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-expm1.csv": 8710594949902856122, - "backend/src/models/dataset_review_pkg/_mapping_models.py": 1407462125644004056, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.cpython-313-x86_64-linux-gnu.so": 13104135217735363438, - "docs/api/html/group__saveLoggingConfig__Function.html": 14654115441279458415, - "docs/api/html/group__test__limited__user__cannot__launch__restricted__operation.html": 3773775103018800523, - "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp.cpython-313-x86_64-linux-gnu.so": 14187094676876471848, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/__init__.pyi": 10548370574281981402, - "docs/api/html/group____resolve__insert__backend.html": 4179734399226191849, - "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/test-failed-1.png": 4602930255871194331, - "venv/lib/python3.13/site-packages/PIL/ImageWin.py": 13635870584232081323, - "docs/api/html/group__generate__commit__message.html": 252727964148470828, - "venv/lib/python3.13/site-packages/jsonschema_specifications/__init__.py": 5732822354953774657, - "venv/lib/python3.13/site-packages/six-1.17.0.dist-info/METADATA": 6502714564886871819, - "docs/api/html/group__deleteUser__Function.html": 7949308553108288142, - "frontend/src/routes/translate/dictionaries/[id]/+page.svelte": 17719561272401762626, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/compat.py": 5871636909854554063, - "venv/lib/python3.13/site-packages/PIL/PalmImagePlugin.py": 13008868719182630239, - "docs/api/html/group____get__record.html": 13908384265392820163, - "docs/api/html/group____FakeTaskManager.html": 3818898705831684798, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/nditer.py": 10038360623032425884, - "venv/lib/python3.13/site-packages/PIL/ImageOps.py": 3884310863548175682, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/dtype.pyi": 12178008080078679769, - "venv/lib/python3.13/site-packages/pandas/core/window/numba_.py": 1444123360986509769, - "venv/lib/python3.13/site-packages/pygments/filter.py": 16846063190466267618, - "venv/lib/python3.13/site-packages/httpcore/_async/interfaces.py": 18250473733460571816, - "frontend/e2e/tests/translation.e2e.js": 9863246540495731784, - "docs/api/html/group__backup__dashboards.html": 13922001482280219867, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arcsinh.csv": 13695545125848146013, - "docs/api/html/group__get__dashboard__tasks__history.html": 3745020964787224389, - "docs/api/html/group__start__maintenance.html": 4069649424458753421, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_base_indexer.py": 3129123098288498633, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/instrumentation.py": 13198832283145198959, - "docs/api/html/navtreeindex1.js": 11503108132424855366, - "venv/lib/python3.13/site-packages/sqlalchemy/types.py": 15819795572817819102, - "docs/api/html/group__test__release__approve__and__publish__revoke__contract.html": 18374074269031186609, - "docs/api/html/group__test__fetchPreview__Function.html": 9834489208067196451, - "docs/api/html/group__DashboardHub__updateColumnFilterSearch__Function.html": 10268710448771778339, - "docs/api/html/group__SupersetClientValidateExportResponse.html": 2041159810613362188, - "venv/lib/python3.13/site-packages/idna/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexer.py": 3658450949169647228, - "docs/api/html/group__SessionEventLoggerImports.html": 4770216863181248837, - "docs/api/html/group____compute__key__hash.html": 364627491871046759, - "venv/lib/python3.13/site-packages/ecdsa/test_ecdh.py": 3338834550392548171, - "docs/api/html/group__GitServiceGitlabMixin.html": 362918173698022720, - "venv/lib/python3.13/site-packages/numpy/fft/tests/test_pocketfft.py": 18025182347224285050, - "backend/tests/test_maintenance_api.py": 2012388734813578069, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/base.cpython-313-x86_64-linux-gnu.so": 8335798324308957127, - "docs/api/html/group__init__db.html": 2707796602248353390, - "docs/api/html/group__run.html": 11476921346246444221, - "docs/api/html/group__auth__callback__adfs.html": 14958207000323215557, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_type_check.py": 3935091606923106753, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/information_schema.py": 8551227132710897346, - "docs/api/html/group__TestClickHouseJoinVerification.html": 7084861590804136181, - "docs/api/html/group____quote__identifier.html": 856007842378984713, - "frontend/src/lib/stores/__tests__/test_datasetReviewSession.js": 8646274524484627210, - "docs/api/html/group__llm__prompt__templates.html": 11148828659101585687, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_django.py": 8455054295577203929, - "docs/api/html/group__test__parse__datetime__none.html": 16098810779566927434, - "docs/api/html/group__delete__conversation.html": 12285162829273366992, - "venv/lib/python3.13/site-packages/anyio/_core/_typedattr.py": 17584691045493791349, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/string/test_astype.py": 10171766562093740708, - "venv/lib/python3.13/site-packages/charset_normalizer/cd.py": 8687667152597941021, - "venv/lib/python3.13/site-packages/numpy/_configtool.py": 2946953194004101078, - "docs/api/html/group__hasPermission__Function.html": 14331056000616172836, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arctanh.csv": 8620268345075425735, - "docs/api/html/group__test__get__logs__with__search.html": 18243052795631026287, - "venv/lib/python3.13/site-packages/rapidfuzz/utils_py.py": 12341806772632276946, - "docs/api/html/group__SupersetClientParseDashboardUrlForFilters.html": 2985094846647626250, - "docs/api/html/group__GitGiteaRoutes.html": 17891704073558062610, - "docs/api/html/group__SupersetContextRecoveryMixin__recover__imported__filters.html": 12238037289621138113, - "docs/api/html/group__CleanComplianceOrchestrator.html": 1417516327802744207, - "venv/lib/python3.13/site-packages/passlib/tests/test_context_deprecated.py": 7876919198015151018, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/__init__.py": 3247870839323520754, - "venv/lib/python3.13/site-packages/sqlalchemy/util/tool_support.py": 4926732415520779988, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/base.py": 5294446834530589760, - "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending_distributions.py": 209769687436320665, - "venv/lib/python3.13/site-packages/PIL/ImageGrab.py": 11114379915762058868, - "docs/api/html/group__formatTime__Function.html": 12061565145373670587, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ma.pyi": 6467636381637286867, - "venv/lib/python3.13/site-packages/yaml/__init__.py": 11044333164208875121, - "docs/api/html/group__test__component__file__exists__Function.html": 7176164496911079412, - "venv/lib/python3.13/site-packages/httpcore/_api.py": 16499985295426060327, - "venv/lib/python3.13/site-packages/httpcore/_async/connection.py": 2155744160496556069, - "docs/api/html/globals_f.html": 11451612662943857082, - "docs/api/html/group__test__get__datasets__invalid__pagination.html": 12210772890706448571, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_tokenizer.py": 7994281991455916540, - "frontend/src/services/git-utils.js": 12982149055119314642, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__main__.py": 8412594868333244844, - "backend/src/api/routes/translate/_router.py": 18140665710457768874, - "docs/api/html/group__TranslateCorrectionTests.html": 1332351481274602438, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_indexing.py": 6039808271042864401, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reset_index.py": 5946666595195952693, - "backend/test_auth.db": 18214842050920965999, - "docs/api/html/group__TaskResponse.html": 12180333889349074765, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_abstract_interface.py": 17625870734108222276, - "docs/api/html/group__NotificationConfig.html": 15289328478261723384, - "backend/src/api/routes/maintenance/_schemas.py": 16377052636841886346, - "venv/lib/python3.13/site-packages/pydantic/datetime_parse.py": 196119761382305574, - "docs/api/html/group__frontend__src__services__storageService__js____downloadFileUrl.html": 12590721399669422477, - "docs/api/html/group__SupersetCompilationAdapter____dump__json.html": 4226532825538873433, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/strptime.cpython-313-x86_64-linux-gnu.so": 12654188486128938170, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/console.py": 3540660073442483692, - "venv/lib/python3.13/site-packages/pygments/lexers/sieve.py": 15757825442941567629, - "venv/lib/python3.13/site-packages/PIL/ImageMath.py": 7395449734007598797, - "docs/api/html/group____make__sync__config__manager.html": 3168926697873935308, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_nat.py": 7400568652984772972, - "venv/lib/python3.13/site-packages/bcrypt/__init__.py": 11415155211917181362, - "backend/src/schemas/__init__.py": 8700952869915187087, - "venv/lib/python3.13/site-packages/pip/_internal/operations/check.py": 16650663913976953800, - "docs/api/html/group__test__migration__dry__run__service__builds__diff__and__risk.html": 8110338995092448857, - "venv/lib/python3.13/site-packages/pandas/core/indexes/range.py": 3838277376774246164, - "docs/api/html/group__build__risks.html": 288765426449485384, - "venv/lib/python3.13/site-packages/numpy/_core/multiarray.pyi": 3494871767602648807, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/mean_.py": 3641008929910474457, - "docs/api/html/group__get__dataset__ids.html": 11409176167791061487, - "docs/api/html/group__GitE2E.html": 5144675262585131661, - "venv/lib/python3.13/site-packages/ecdsa/_compat.py": 13034064768731879065, - "venv/lib/python3.13/site-packages/pydantic/_internal/_utils.py": 10936299012115637790, - "docs/api/html/group__test__log__persistence.html": 9322876502081854485, - "venv/lib/python3.13/site-packages/numpy/lib/stride_tricks.pyi": 5879290708972838025, - "venv/lib/python3.13/site-packages/fastapi/__main__.py": 10755252341326986079, - "docs/api/html/group__SupersetPreviewPipelineTests.html": 7525293242596376133, - "docs/api/html/group__isFeatureEnabled__Function.html": 11649420265376793257, - "docs/api/html/group__get__role__by__name.html": 15153180444289216375, - "docs/api/html/group__APIClient.html": 1707825928665932322, - "docs/api/html/group__HealthMatrix.html": 10134874641663847032, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cosh.csv": 9287774220881887502, - "venv/lib/python3.13/site-packages/pygments/unistring.py": 18037463865539157041, - "docs/api/html/group__ApprovalState.html": 16834628662566561513, - "docs/api/html/group__test__list__runs__success.html": 10465705390005371941, - "venv/lib/python3.13/site-packages/pandas/io/parsers/arrow_parser_wrapper.py": 11745253171984606573, - "docs/api/html/group__compute__dict__snapshot__hash.html": 15191419945274873380, - "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/cffi/model.py": 13544803366359102016, - "docs/api/html/group__mock__env__public__Module.html": 11925751473549536485, - "docs/api/html/group__frontend__src__lib__auth__store__ts____AuthState.html": 725779176116101299, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_ufunclike.py": 13752381070039804384, - "venv/lib/python3.13/site-packages/pillow.libs/libjpeg-32d42e18.so.62.4.0": 11717944412021329207, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_isin.py": 11330491145602008796, - "docs/api/html/group__add__translation__job.html": 715901058035169272, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_sh_gcc.h": 3574353221233465215, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_searchsorted.py": 12074107109575676897, - "docs/api/html/group__LLMClient.html": 17661474179652909834, - "venv/lib/python3.13/site-packages/pandas/io/json/_normalize.py": 15938855324762935595, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/interfaces.py": 5371646629319169153, - "venv/lib/python3.13/site-packages/_pytest/_io/saferepr.py": 8025234471485188730, - "frontend/tests/maintenance-api.test.ts": 10196278218740408254, - "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py": 14441258905942300095, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/WHEEL": 2357997949040430835, - "backend/git_repos/remote/test-repo.git/objects/e9/1109abe29d5228675fecd6fc38a097147c090d": 15716642455993350072, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_values.py": 8372572954663628844, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/einsumfunc.py": 9944042006439149561, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/_conditional.py": 14190849361275154667, - "venv/lib/python3.13/site-packages/pydantic/warnings.py": 5142352784799639092, - "docs/api/html/group__create__remote__repository.html": 1939587202210625796, - "venv/lib/python3.13/site-packages/ecdsa/test_malformed_sigs.py": 2606295770491528880, - "venv/lib/python3.13/site-packages/greenlet/greenlet_allocator.hpp": 11041111583319491309, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_backend.py": 17793139799410518847, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/uts46data.py": 1867240043777383728, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/zip-safe": 15240312484046875203, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/authorization_server.py": 6858244712625056421, - "venv/lib/python3.13/site-packages/pandas/plotting/_misc.py": 10202078355301731720, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_ticks.py": 11649965080303468085, - "venv/lib/python3.13/site-packages/pytest/__main__.py": 8747175113746141224, - "docs/api/html/group__test__revoked__key__raises__401.html": 10905401151898512037, - "docs/api/html/group__frontend__src__lib__api__validation__js____fetchTasks.html": 2813676835730064546, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_tolist.py": 8343058000369672317, - "venv/lib/python3.13/site-packages/jose/constants.py": 10880650415673457194, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_base.py": 15292769510056833117, - "venv/lib/python3.13/site-packages/jsonschema/_keywords.py": 949562792260528762, - "docs/api/html/globals_p.html": 5091452083134997558, - "backend/src/core/superset_client/_base.py": 8704044125468217140, - "docs/api/html/group__Select.html": 13106318215518617964, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/errors.py": 12935154705443176852, - "docs/api/html/group__ScheduleSchema.html": 6081468916671081411, - "docs/api/html/group__load__bootstrap__artifacts.html": 12225841342507628272, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Postfix_py.py": 6653895521961110080, - "venv/lib/python3.13/site-packages/numpy/lib/recfunctions.pyi": 10487441680587530350, - "docs/api/html/group__pull__changes.html": 14394506441825881379, - "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.py": 4037074830103951000, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_hashing.py": 11119284926039008503, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/__init__.py": 14268033439436797364, - "venv/lib/python3.13/site-packages/greenlet/greenlet_thread_support.hpp": 7773996382104849037, - "venv/lib/python3.13/site-packages/pydantic/v1/color.py": 14545509649623975912, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_like.pyi": 1968570015765106620, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.py": 3654583356259850174, - "docs/api/html/group__get__dashboard__detail.html": 10495893496001819056, - "docs/api/html/globals_a.html": 15978428091419449896, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_category.py": 3944898528119364040, - "venv/lib/python3.13/site-packages/numpy/f2py/cfuncs.py": 10803011906582324302, - "venv/lib/python3.13/site-packages/pygments/lexers/bibtex.py": 14652372822443154388, - "venv/lib/python3.13/site-packages/pandas/compat/_constants.py": 3909166275649444824, - "docs/api/html/group__oauth2__scheme__optional.html": 10907588018100611451, - "docs/api/html/group__handleSelectionChange__Function.html": 3955497001431142366, - "docs/api/html/group____pii__imports.html": 1295891648143980104, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_join.py": 1131792466825856990, - "docs/api/html/group__INITIAL__PERMISSIONS.html": 5081007697300821170, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/utils.py": 7707707044867513457, - "docs/api/html/search/functions_3.js": 415719142770932975, - "docs/api/html/navtreeindex20.js": 6185764429877814972, - "venv/lib/python3.13/site-packages/numpy/lib/npyio.pyi": 3212440968296555560, - "docs/api/html/search/all_4.js": 1005255247300836207, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/aioodbc.py": 2018840826888846321, - "docs/api/html/group__resolve__trusted__policy__snapshots.html": 1966325452563273431, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/array_constructors.pyi": 164538008396001632, - "venv/lib/python3.13/site-packages/pandas/core/indexes/accessors.py": 14394872772838047912, - "frontend/build/_app/immutable/nodes/29.B0pLSJVu.js": 10616222217908613536, - "frontend/build/_app/immutable/chunks/CyXSNadK.js": 14938368543846038343, - "venv/lib/python3.13/site-packages/jose/backends/base.py": 12795468809472351388, - "docs/api/html/group____repo__with__seed__data.html": 17628026577432977931, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/compat.py": 17520043776344667309, - "docs/api/html/group______tests______test__task__logger.html": 3022509304089562027, - "docs/api/html/group__StartCheckRequest.html": 3154093382504996764, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_index_tricks.py": 4676293127859087991, - "venv/lib/python3.13/site-packages/cffi/vengine_cpy.py": 10684192171370688079, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_feather.py": 13613400750755530364, - "venv/lib/python3.13/site-packages/pandas/core/arraylike.py": 11439602957100017713, - "venv/lib/python3.13/site-packages/pip/_internal/models/direct_url.py": 4619204214785230341, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/conftest.py": 16571782049241458769, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_integer.f90": 8308927438994838783, - "docs/api/html/group____build__api__url.html": 12259647430202722901, - "docs/api/html/group__RoleSchema.html": 3555001744310629858, - "venv/lib/python3.13/site-packages/_pytest/config/argparsing.py": 11156052029258891503, - "venv/lib/python3.13/site-packages/pydantic/_internal/_docs_extraction.py": 5210114991139191634, - "venv/lib/python3.13/site-packages/rpds/__init__.pyi": 12037474523494903587, - "venv/lib/python3.13/site-packages/pydantic/v1/version.py": 2758971830527526972, - "venv/lib/python3.13/site-packages/pydantic_core/__init__.py": 10563134104783445328, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_ufunc.py": 669019702416198889, - "docs/api/html/group__test__no__active__events.html": 18077367180380066108, - "venv/lib/python3.13/site-packages/keyring/backends/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/starlette/routing.py": 16063812008141874150, - "docs/api/html/group__test__update__task__success.html": 12948386744398037759, - "venv/lib/python3.13/site-packages/numpy/_core/_multiarray_tests.cpython-313-x86_64-linux-gnu.so": 442168371609469538, - "docs/api/html/group__resolveDashboardRef__Function.html": 11580641610445169138, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resample_api.py": 3278209206670699265, - "venv/lib/python3.13/site-packages/pycparser/ply/yacc.py": 4218889705926129841, - "frontend/src/routes/settings/git/__tests__/git_settings_page.ux.test.js": 3774268544046391936, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/INSTALLER": 17282701611721059870, - "backend/src/plugins/llm_analysis/plugin.py": 11133953577515642529, - "frontend/src/lib/components/dataset-review/__tests__/validation_findings_panel.ux.test.js": 16008187945694980240, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_set.py": 534498057184394558, - "docs/api/html/group__SupersetClientExtractUserDisplay.html": 5365697524336802629, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/__init__.py": 3673322722277252961, - "docs/api/html/group__TranslationRun.html": 13758493070000589779, - "frontend/src/components/MappingTable.svelte": 16339628752309005366, - "venv/lib/python3.13/site-packages/git/repo/base.py": 13975465345838683533, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/METADATA": 10220680536734745103, - "docs/api/html/group__SidebarNavigation__Module.html": 15358237226164093988, - "venv/lib/python3.13/site-packages/pillow.libs/libxcb-64009ff3.so.1.1.0": 7322566017497612912, - "venv/pyvenv.cfg": 2513607356761047446, - "docs/api/html/group__RoutesTestsConftest.html": 1558041571936568459, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/tools.py": 6437315459200419827, - "venv/lib/python3.13/site-packages/pandas/core/computation/parsing.py": 3084943521520476999, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_matmul.py": 9323220618335072725, - "docs/api/html/group__AppModule.html": 7571679846299956443, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_fillna.py": 9297637486401098611, - "docs/api/html/globals_j.html": 16316103223566935047, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/licenses/LICENSE": 13322127602069971876, - "frontend/src/lib/components/layout/__tests__/sidebarNavigation.test.js": 1278883813018181993, - "docs/api/html/group__reflect.html": 5691625797786994210, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh25337/data.f90": 13833561828029901615, - "venv/lib/python3.13/site-packages/pygments/lexers/soong.py": 10273453675034306814, - "frontend/src/lib/components/translate/__tests__/test_translation_preview.svelte.js": 2673586123384011532, - "docs/api/html/group__is__adfs__configured.html": 6773671149149811294, - "frontend/src/components/llm/DocPreview.svelte": 14349224708133169902, - "docs/api/html/group__normalize__llm__settings.html": 8684454067738894675, - "docs/api/html/group__src__api.html": 13647182032403294242, - "docs/api/html/search/groups_18.js": 13110703267329058335, - "backend/tests/test_models.py": 14571471179954305607, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/__init__.py": 8558541715982163200, - "venv/lib/python3.13/site-packages/pygments/lexers/shell.py": 3842338968014057744, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress.py": 5951745251426856398, - "frontend/src/lib/components/ui/SearchableMultiSelect.svelte": 1872229619122554196, - "docs/api/html/group__websocket__endpoint.html": 5701303797721389053, - "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/WHEEL": 9788895026336324100, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/__init__.py": 4265012785873472600, - "docs/api/html/group__background__tasks.html": 7187534320763437047, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/WHEEL": 16097436423493754389, - "docs/api/html/group__DashboardTypes__Module.html": 17788793500352327911, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop_duplicates.py": 5874427993953991216, - "backend/src/plugins/translate/service.py": 12396293729608729912, - "venv/lib/python3.13/site-packages/PIL/ImageMode.py": 2361901066832921091, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_functions.py": 16376319335326812580, - "venv/lib/python3.13/site-packages/psycopg2/errors.py": 10441360078612452560, - "docs/api/html/group__CleanReleaseContracts.html": 13165007053860558990, - "venv/lib/python3.13/site-packages/uvicorn/protocols/http/auto.py": 15947958974558543657, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/keys.pyi": 1846673463183660140, - "docs/api/html/group__DatasetReviewSessionRepository.html": 6205499016168304119, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string.py": 3834067244989779682, - "venv/lib/python3.13/site-packages/numpy/_core/shape_base.py": 3996519099530770308, - "frontend/build/_app/immutable/chunks/aniH9ONl.js": 11297774342683903539, - "docs/api/html/group__frontend__src__lib__api__validation__js____fetchTask.html": 7928201447544956738, - "docs/api/html/group__uploadFile__Function.html": 2179256576386234524, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_datetime.py": 7456744745424297814, - "docs/api/html/group__test__commit__changes__applies__profile__identity__before__commit.html": 7275902928560381122, - "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pip/_internal/models/pylock.py": 16697828625021519459, - "venv/lib/python3.13/site-packages/pandas/io/formats/__init__.py": 18140261896107421679, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_asof.py": 18383318159051131669, - "backend/src/api/routes/git/_deps.py": 11478430655620147674, - "venv/lib/python3.13/site-packages/numpy/_core/_methods.pyi": 9155045948490225716, - "docs/api/html/group____await__none.html": 14771125371408463258, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/mock.py": 14010776046610111182, - "venv/lib/python3.13/site-packages/_pytest/tmpdir.py": 10834220332037364219, - "venv/lib/python3.13/site-packages/PIL/_typing.py": 17407980360703360848, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/_initialize.py": 15511055314565873928, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_constructors.py": 2786493777737472376, - "docs/api/html/group__AssistantMessageRecord.html": 3626244854239183247, - "docs/api/html/group__ContextAwarePromptBuilder.html": 7714816079153271995, - "frontend/src/components/TaskList.svelte": 17511781335293490363, - "docs/api/html/group__add__log.html": 5531300137956566412, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_cumulative.py": 8178093067175155877, - "docs/api/html/group__get__user__by__id.html": 7396712021803572982, - "docs/api/html/group__AuthRepositoryModule.html": 8460217615305414048, - "models": 14346352665111990852, - "docs/api/html/group____mask__sensitive__text.html": 10707233070284578212, - "docs/api/html/group____get__git__config__or__404.html": 9100308666057061844, - "docs/api/html/group__test__end__not__found.html": 15644048943441130439, - "venv/lib/python3.13/site-packages/dotenv/cli.py": 7952654621986067973, - "docs/api/html/group__test__sync__environment__updates__existing__mapping.html": 5151396558537904785, - "docs/api/html/group__app__middleware.html": 1146307130445661, - "docs/api/html/group__IdMappingService.html": 128548327933737637, - "venv/lib/python3.13/site-packages/fastapi/middleware/__init__.py": 5289703478893407762, - "venv/lib/python3.13/site-packages/numpy/ma/README.rst": 13203411621071845216, - "venv/lib/python3.13/site-packages/fastapi/concurrency.py": 17937688838625807004, - "frontend/src/routes/profile/__tests__/profile-preferences.integration.test.js": 10120412205695269578, - "docs/api/html/group__ComplianceStageBase.html": 5577816396402425153, - "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_generation_shared.py": 23345689300073549, - "docs/api/html/group__GitServiceUrlMixin.html": 4060641084492531477, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarrayobject.h": 7320510589635636690, - "docs/api/html/group__handleMappingUpdate__Function.html": 7438288773532136469, - "docs/api/html/group__ConflictResolution.html": 8175611913139451582, - "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.py": 1472021608501071055, - "docs/api/html/group__update__dashboard__layout.html": 2290775089034614539, - "venv/lib/python3.13/site-packages/passlib/tests/backports.py": 17098448557554464521, - "venv/lib/python3.13/site-packages/pygments/lexers/_usd_builtins.py": 8671446144574174964, - "venv/lib/python3.13/site-packages/pydantic/generics.py": 15019381413806459410, - "docs/api/html/group__reject__candidate__endpoint.html": 15716705953047471464, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.c": 11535375410441493281, - "venv/lib/python3.13/site-packages/authlib/common/errors.py": 16082974228565515177, - "docs/api/html/group__detect__virtual__columns.html": 7311963466156624138, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/numpyconfig.h": 13339221870252345205, - "backend/src/services/dataset_review/orchestrator_pkg/_commands.py": 2554792474786179775, - "venv/lib/python3.13/site-packages/_pytest/_code/code.py": 5248836309618416369, - "docs/api/html/group__get__session__detail.html": 2777345426267776065, - "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/requests.py": 6703055041957748929, - "docs/api/html/group____build__chart__template__pool.html": 18077972997939014879, - "docs/api/html/group__DictionaryEntry.html": 16163430495584463690, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/hkdf.py": 8539232219382152238, - "venv/lib/python3.13/site-packages/numpy/exceptions.pyi": 13942486291715303235, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/base_server.py": 9643197259080504542, - "venv/lib/python3.13/site-packages/passlib/tests/test_hosts.py": 8284535274820610802, - "docs/api/html/group__extract__native__filters__from__key__async.html": 5896768597674544567, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_is_full.py": 9577318758326279694, - "venv/lib/python3.13/site-packages/pygments/styles/friendly.py": 13799024964803741891, - "docs/api/html/group__SupersetClientExtractNativeFiltersFromKey.html": 15741298745054631643, - "docs/api/html/group____normalize__dashboard__owner__values.html": 5450496100604102874, - "venv/lib/python3.13/site-packages/bcrypt-4.0.1.dist-info/LICENSE": 11167201188673981085, - "docs/api/html/group____sanitize__optional__identity__value.html": 6453024202123093607, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_common.py": 3559416690270529904, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__main__.py": 6480933938145689964, - "docs/api/html/group__TranslationRunResult.html": 15863382174730965712, - "venv/lib/python3.13/site-packages/pycparser/c_generator.py": 14922277346364091122, - "docs/api/html/group__buildParams__Function.html": 18347180877148300889, - "docs/api/html/group__loadEnvironments__Function.html": 5584041378316217605, - "docs/api/html/group__test__task__manager.html": 16353988531673977488, - "docs/api/html/group__update__global__settings.html": 14939400432665122240, - "docs/api/html/group__ValidationTaskUpdate.html": 5117191705439943824, - "venv/bin/pytest": 2593802493707545818, - "frontend/src/lib/components/health/HealthMatrix.svelte": 18141816318382901531, - "docs/api/html/group____ensure__dataset__review__session__columns.html": 16968513755674644702, - "venv/lib/python3.13/site-packages/authlib/oauth1/__init__.py": 3692257288065292607, - "docs/api/html/group__test__enterprise__policy__missing__prohibited.html": 9934851722239315434, - "docs/api/html/group__MaintenanceApi.html": 10684759386844628447, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/compiler.py": 11662494978292401568, - "venv/lib/python3.13/site-packages/_pytest/reports.py": 13930709338396699351, - "docs/api/html/group__test__repository__rejects__report__overwrite__for__same__report__id.html": 12621310059402333179, - "docs/api/html/group__cancelAssistantOperation__Function.html": 1438697842894474195, - "docs/api/html/group__duplicate__job.html": 4139685743055526761, - "docs/api/html/group__test__create__user.html": 11963320393863284451, - "venv/lib/python3.13/site-packages/psycopg2/_psycopg.cpython-313-x86_64-linux-gnu.so": 13471342289207537479, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_endian.h": 2761502363895204652, - "docs/api/html/group__Counter.html": 5547720716432013609, - "backend/src/core/superset_client/_dashboards_list.py": 18014687561732553425, - "docs/api/html/group__SupersetCompilationAdapter__mark__preview__stale.html": 7887865650371630867, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____setTaskDrawerAutoOpenPreference.html": 8925927322956055906, - "docs/api/html/group____mock__env.html": 6811360979523770361, - "docs/api/html/group__delete__environment.html": 14176437049644341788, - "docs/api/html/group__handleRunDebug__Function.html": 17645482735921964594, - "docs/api/html/group__handleSelectAll__Function.html": 14242974889351038882, - "docs/api/html/group__test__submit__correction__overwrite.html": 5210632411258869706, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/__init__.py": 18036348770966744795, - "venv/lib/python3.13/site-packages/pandas/_config/config.py": 1494288055210911451, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_tree.py": 15789443159423302291, - "docs/api/html/group__Debounce__Module.html": 15506061807938542793, - "docs/api/html/group__test__pull__changes__applies__profile__identity__before__pull.html": 16120757619270766931, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/__init__.py": 14571251344510763303, - "frontend/src/components/git/__tests__/git_manager.unfinished_merge.integration.test.js": 10735454478982675155, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/__init__.py": 15130871412783076140, - "backend/src/services/notifications/providers.py": 12802217818475476969, - "backend/git_repos/remote/test-repo.git/hooks/sendemail-validate.sample": 8058833260784074657, - "venv/lib/python3.13/site-packages/numpy/testing/_private/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/bulk_persistence.py": 2307818727114646866, - "docs/api/html/group__ProfileSchemas.html": 17384165309593912417, - "docs/api/html/group__DashboardHub__selectAllColumnFilterValues__Function.html": 12068777279564411579, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__ufunc_api.h": 4516275133897904047, - "venv/lib/python3.13/site-packages/numpy/testing/_private/extbuild.pyi": 18253950285509265097, - "venv/lib/python3.13/site-packages/_pytest/config/__init__.py": 12787579251203510616, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_indexing.py": 4334578480714520407, - "docs/api/html/group__test__happy__path.html": 16218633066560416160, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/_typing.py": 10524021526716767489, - "docs/api/html/group__TestSecurityAuditFixes.html": 9553894818504109298, - "venv/lib/python3.13/site-packages/greenlet/TPythonState.cpp": 12828594265749606443, - "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.pyi": 7674087576653964854, - "venv/lib/python3.13/site-packages/pandas/_libs/json.pyi": 10989301892466547548, - "backend/src/core/utils/superset_context_extractor/_filters.py": 14230663187221307078, - "docs/api/html/group____templates__imports.html": 12808569599321695443, - "venv/lib/python3.13/site-packages/rapidfuzz/_utils.py": 14374663810542207121, - "docs/api/html/group____flush__logs.html": 17794969687461313535, - "docs/api/html/group____json__load__if__needed.html": 17553294735423274387, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/conftest.py": 8406022190801970256, - "docs/api/html/group__test__test__git__config__fails__validation.html": 14329115310381033327, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/style.py": 15214295655795348821, - "venv/lib/python3.13/site-packages/gitpython-3.1.46.dist-info/RECORD": 6187512110158566509, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_function_base.py": 16624804022675533740, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval_pyarrow.py": 3849382723890414899, - "backend/src/services/maintenance/_banner_renderer.py": 17250656767428031478, - "docs/api/html/search/functions_f.js": 17213773482888625604, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/timedeltas/test_constructors.py": 17219764538851313252, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/filewrapper.py": 17786759398959482432, - "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pyi": 15790738442952696034, - "docs/api/html/group____get__run__service.html": 2727986697505048178, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/array.py": 13040461386712513615, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/utils.py": 8633356372647820801, - "venv/lib/python3.13/site-packages/fastapi/encoders.py": 1914192268055730299, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/__init__.py": 1197579313959289096, - "venv/lib/python3.13/site-packages/pygments/styles/material.py": 14325514369696961874, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/RECORD": 4857922668040624663, - "docs/api/html/group__test__candidate__manifest__services.html": 2472733304553637078, - "docs/api/html/navtree.js": 13579380893835360126, - "frontend/src/components/tools/DebugTool.svelte": 8095371084479333656, - "frontend/build/_app/immutable/nodes/16.91GU_ND0.js": 13359394192978348680, - "backend/src/api/routes/assistant/__init__.py": 16856916031113491888, - "venv/lib/python3.13/site-packages/pandas/io/formats/html.py": 11849941990375030601, - "docs/api/html/group__localize.html": 16532518584734972010, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.obj": 11967637259540485652, - "frontend/src/lib/components/assistant/__tests__/assistant_confirmation.integration.test.js": 12582020574293031526, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_dst.py": 12243642467695371088, - "venv/lib/python3.13/site-packages/pygments/lexers/javascript.py": 16869490851285457857, - "venv/lib/python3.13/site-packages/uvicorn/supervisors/__init__.py": 15971002342788501945, - "docs/api/html/group__frontend__e2e__helpers__api__helper__js____apiPut.html": 539702435765100928, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/python3.npy": 4957143805477405905, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c": 17333399794446198412, - "docs/api/html/group__TaskRecord.html": 17484476379866416581, - "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/WHEEL": 13232065379159720345, - "backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py": 7304392396276280230, - "docs/api/html/group__DictionaryResponse.html": 17051408790475508683, - "venv/lib/python3.13/site-packages/pandas/core/indexes/api.py": 13750417165301467381, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_tz_localize.py": 4937713043686953098, - "docs/api/html/group__run__reject.html": 17845346756984073576, - "docs/api/html/group__TranslationPreviewRecord.html": 12396412092000079349, - "docs/api/html/group__DatasetColumn.html": 6676823301052927790, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/events.py": 10224214051422678828, - "venv/lib/python3.13/site-packages/pygments/lexers/clean.py": 17772169928835025327, - "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.py": 18402126171936308228, - "docs/api/html/group__SupersetAccountLookupRequest.html": 11189418222429477505, - "docs/api/html/group__ManifestItem.html": 17134684545331977423, - "venv/lib/python3.13/site-packages/numpy/_core/tests/examples/limited_api/meson.build": 8784756284679859367, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_sort_values.py": 834951665649981397, - "venv/lib/python3.13/site-packages/pygments/styles/stata_dark.py": 324072437809944751, - "frontend/src/lib/stores/__tests__/mocks/navigation.js": 16028014574825445651, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/__multiarray_api.h": 16138871162713175599, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_reductions.py": 2816859231048302619, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraysetops.pyi": 1782641795267171928, - "docs/api/html/group__DashboardProjection.html": 9806475663684458617, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/requests.py": 1102963893193914979, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_fillna.py": 16297834601116528166, - "venv/lib/python3.13/site-packages/pygments/lexers/jsx.py": 13855228003748707341, - "backend/src/schemas/validation.py": 10970778481296603249, - "venv/lib/python3.13/site-packages/pluggy/_callers.py": 1042673447718196223, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/test_as_unit.py": 4283520188709399244, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_polynomial.py": 3049262771098624825, - "docs/api/html/group__APIKeyPrincipal.html": 17508654367012660787, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-sinh.csv": 12358602501260667480, - "venv/lib/python3.13/site-packages/starlette/__init__.py": 3252794766897205870, - "docs/api/html/group__create__branch.html": 8681906529509434557, - "docs/api/html/group____RecordingNetworkClient.html": 10311785607548179860, - "venv/lib/python3.13/site-packages/cffi/cparser.py": 5246372077864988755, - "venv/lib/python3.13/site-packages/pluggy/_warnings.py": 2664977718908223842, - "venv/lib/python3.13/site-packages/passlib/tests/test_handlers.py": 4940305787631493030, - "venv/lib/python3.13/site-packages/gitdb/stream.py": 3035738824550231197, - "docs/api/html/group__main.html": 12159712531135979472, - "venv/lib/python3.13/site-packages/pandas/core/common.py": 14578909424716153057, - "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_gather.py": 3634138701088855517, - "docs/api/html/group____EXT__frontend__AssistantChatTest____Module.html": 3650932694801681064, - "venv/lib/python3.13/site-packages/pandas-3.0.1.dist-info/METADATA": 15658158392301945427, - "docs/api/html/group____extract__dialect.html": 17170247248753677907, - "venv/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py": 10505346210114745037, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename_axis.py": 13193478481758930128, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py": 13021437050542691118, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayterator.pyi": 15722265241660279277, - "backend/src/plugins/llm_analysis/scheduler.py": 16461360386665667203, - "backend/src/core/migration_engine.py": 4215834818829537236, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/test_empty.py": 6845813091876023610, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/secretstorage/collection.py": 17903258621904912493, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_sparc_sun_gcc.h": 2915128971664712180, - "venv/lib/python3.13/site-packages/numpy/core/_multiarray_umath.py": 10973016138581934035, - "docs/api/html/group____latest__publication__for__candidate.html": 3876828569531437570, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__serialize__preview__record.html": 17364001740956652340, - "venv/lib/python3.13/site-packages/starlette/background.py": 10232175205397808417, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_series.py": 2980850649986213051, - "docs/api/html/group__build__distribution__manifest.html": 9519354018984408473, - "venv/lib/python3.13/site-packages/cryptography/x509/extensions.py": 10197248868682803019, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_style.py": 10405592363651914796, - "docs/api/html/group__handleTaskClick__Function.html": 11752031450688903634, - "docs/api/html/search/functions_b.js": 1263949438112498611, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pycparser/ast_transforms.py": 9032453625977464506, - "docs/api/html/nav_g.png": 6882746348872232672, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/apps.py": 10758062668702044519, - "docs/api/html/group__MaintenanceStartRequest.html": 13433532661036125704, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/etcd.py": 4078354986605069279, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_m68k_gcc.h": 6580173800099450404, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/block_docstring/foo.f": 2489756567993745076, - "venv/lib/python3.13/site-packages/secretstorage/exceptions.py": 16701299261122689924, - "venv/lib/python3.13/site-packages/authlib/common/urls.py": 13627272625165922482, - "frontend/build/_app/immutable/chunks/Ci0knLe8.js": 727608219162115036, - "docs/api/html/group__get__preview__records.html": 2916851160069655824, - "venv/lib/python3.13/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE": 13479831069780783137, - "docs/api/html/group__handleViewLogs__Function.html": 3848593199895136202, - "venv/lib/python3.13/site-packages/pandas/core/window/rolling.py": 6316477981414191778, - "venv/lib/python3.13/site-packages/numpy/random/_bounded_integers.cpython-313-x86_64-linux-gnu.so": 16604252871044637047, - "docs/api/html/group__TaskManager.html": 14062251301224084851, - "docs/api/html/search/groups_e.js": 12304992592735263012, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayprint.pyi": 2424868797160722318, - "frontend/playwright-report/trace/codeMirrorModule.DYBRYzYX.css": 14866915564324637173, - "docs/api/html/group__openEditModal__Function.html": 6284886960680022829, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/flatiter.py": 3066469329738423021, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/libdivide.h": 5314232500579481313, - "docs/api/html/group__assistantChatStore__tests__Function.html": 434816894896782034, - "venv/lib/python3.13/site-packages/sqlalchemy/inspection.py": 8563914460959280101, - "venv/lib/python3.13/site-packages/pygments/lexers/julia.py": 13516407944275342328, - "venv/lib/python3.13/site-packages/pygments/lexers/graph.py": 10445044822847205061, - "docs/api/html/group__DictionaryImport.html": 17459309263375084602, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/__init__.py": 11121941644106061453, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_replace.py": 8784361132920473760, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setDirty.html": 6652561854785184600, - "venv/lib/python3.13/site-packages/pip/_internal/commands/list.py": 13035925292530461722, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/revocation.py": 16178499278312777474, - "docs/api/html/group__SupersetClientFetchTotalObjectCount.html": 6342114507024365275, - "venv/lib/python3.13/site-packages/pandas/tests/extension/array_with_attr/__init__.py": 15991623390785635773, - "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/__init__.py": 13584649922093198724, - "docs/api/html/group__extract__native__filters__from__permalink__async.html": 7707192445251368687, - "venv/lib/python3.13/site-packages/pillow-12.1.1.dist-info/RECORD": 17545477863793260029, - "venv/lib/python3.13/site-packages/yaml/nodes.py": 3463622922914760653, - "backend/src/models/clean_release.py": 4198911944577810567, - "backend/src/services/clean_release/__tests__/test_report_builder.py": 11856874434357958029, - "backend/src/plugins/mapper.py": 12075913231649127940, - "docs/api/html/docd.svg": 5892816938133793015, - "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/video.webm": 10645096005049255831, - "docs/api/html/group__mapping__service.html": 1146234231481143720, - "docs/api/html/group__ExtraFormDataMerge.html": 4791611138331694386, - "docs/api/html/group__DatasetReviewProfileModels.html": 9977462433342924108, - "backend/src/services/profile_preference_service.py": 15980302453836921814, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_iteration.py": 2118501258411211851, - "docs/api/html/group__suggest__mappings.html": 12832332337511809353, - "docs/api/html/search/groups_3.js": 13945080001959834321, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_day.py": 6371851881186970754, - "docs/api/html/group__DashboardItem.html": 6170980058100567138, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_builtin_md4.py": 16840174243259839994, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_nep50_promotions.py": 6318578947214302182, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/tree.py": 4991806303236874829, - "docs/api/html/group__VariableKind.html": 14364200722064448242, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_antijoin.py": 14435396877404907826, - "docker/backend.Dockerfile": 3370913626880503214, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nbit_base_example.pyi": 8571452382806614924, - "docs/api/html/group__ClarificationQuestion.html": 5390726230912714507, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_timedeltas.py": 4118671843063545221, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/totp.py": 11888568940387752607, - "docs/api/html/group__test__get__dashboards__not__found.html": 6573102849582362504, - "docs/api/html/group__test__get__users__page__uses__fallback__endpoint__when__primary__fails.html": 476657279500282456, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py": 2628895559914914693, - "docs/api/html/group____normalize__term.html": 2393952213994759345, - "venv/lib/python3.13/site-packages/pandas/core/indexers/utils.py": 5861072573716897447, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_indexing.py": 81180636019259296, - "venv/lib/python3.13/site-packages/pandas/core/reshape/melt.py": 14109947554124551986, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_count.py": 16947194157984708529, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_expanding.py": 9047466276603454007, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/period.cpython-313-x86_64-linux-gnu.so": 14468234464367691795, - "venv/lib/python3.13/site-packages/pandas/tests/tools/test_to_numeric.py": 18037815278545274402, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pygments/styles/lilypond.py": 12486578704132023691, - "backend/src/services/__tests__/test_llm_provider.py": 14345242680566878510, - "docs/api/html/group__ValidationSchemas.html": 15986503582536227156, - "venv/lib/python3.13/site-packages/pip/_internal/main.py": 13711402795965346761, - "docker/all-in-one.Dockerfile": 10896793266219904099, - "venv/lib/python3.13/site-packages/pycparser/ply/ctokens.py": 1599952198962458730, - "venv/lib/python3.13/site-packages/ecdsa/test_rw_lock.py": 1572802971060634697, - "docs/api/html/group__test__create__git__config__persists__config.html": 1075563316969273922, - "venv/lib/python3.13/site-packages/websockets/http11.py": 11022534176398053082, - "venv/lib/python3.13/site-packages/pip/_internal/__init__.py": 7060345458485901709, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/METADATA": 732041908491732594, - "venv/lib/python3.13/site-packages/pip/py.typed": 10243553515949422671, - "venv/lib/python3.13/site-packages/python_multipart/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayobject.py": 13337664596833150958, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/cells.py": 8299309127027168194, - "backend/src/api/routes/git/_gitea_routes.py": 3966408809424419232, - "docs/api/html/group____authorize__intent.html": 14955083261995312080, - "docs/api/html/group__get__mappings.html": 9283879926950218053, - "frontend/src/lib/components/layout/__tests__/test_sidebar.svelte.js": 15166117912533507471, - "venv/lib/python3.13/site-packages/pygments/lexers/foxpro.py": 14032656072384708293, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_update.py": 9098258267671647783, - "docs/api/html/group__Sidebar.html": 9352378737121268807, - "venv/lib/python3.13/site-packages/requests/sessions.py": 6936640856092320928, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.py": 15426828792217499138, - "docs/api/html/group__test__expired__confirmation__cannot__be__confirmed.html": 1867999819644325388, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarinherit.py": 15240535501243623986, - "venv/lib/python3.13/site-packages/passlib/tests/__init__.py": 16543763165305017760, - "venv/lib/python3.13/site-packages/pygments/formatters/_mapping.py": 13335227913146542481, - "backend/git_repos/remote/test-repo.git/config": 14478893374498894766, - "venv/lib/python3.13/site-packages/greenlet/CObjects.cpp": 3454797518210223926, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/cast.py": 342228968652072052, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_datetime.py": 4179112383995346046, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_musllinux.py": 2423350951094915747, - "venv/lib/python3.13/site-packages/jsonschema/cli.py": 9813914882090789748, - "docs/api/html/group__DashboardHub__normalizeOwners__Function.html": 11075995940263529029, - "docs/api/html/group__TestClickHouseEndToEnd.html": 10329259316133049765, - "backend/src/plugins/translate/__tests__/__init__.py": 2490542523242946173, - "venv/lib/python3.13/site-packages/pandas/core/_numba/extensions.py": 4132402557064663751, - "docs/api/html/group__MetricSnapshot.html": 18425939041842608851, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_errors.py": 3301889847545579393, - "docs/api/html/group__TranslateDatasourcesApi.html": 16762087010624520764, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/integration.py": 12670848015915724967, - "venv/lib/python3.13/site-packages/websockets/utils.py": 17624459810483092475, - "docs/api/html/group__SupersetClientSanitizeUserText.html": 6776437542055180978, - "venv/lib/python3.13/site-packages/pydantic/v1/fields.py": 3948219773415054006, - "backend/src/api/routes/assistant/_schemas.py": 17540735345133623114, - "docs/api/html/group__frontend__src__lib__i18n__index__ts______.html": 15852621729962062852, - "venv/lib/python3.13/site-packages/pydantic/root_model.py": 20625057828723317, - "docs/api/html/group__shutdown__event.html": 11840402120515204160, - "venv/lib/python3.13/site-packages/pygments/styles/lovelace.py": 15082819162722685616, - "docs/api/html/group__get__schema.html": 13895060400411219452, - "venv/lib/python3.13/site-packages/PIL/_imagingtk.cpython-313-x86_64-linux-gnu.so": 1708198947135304326, - "backend/src/services/dataset_review/repositories/repository_pkg/_mutations.py": 968076476163114767, - "venv/lib/python3.13/site-packages/pip/_internal/utils/egg_link.py": 18211947508793805393, - "docs/api/html/group__test__parse__dashboard__url__for__filters__native__key__slug__resolution__fails.html": 5436713915568993137, - "venv/lib/python3.13/site-packages/gitdb/util.py": 645937410168583692, - "docs/api/html/group__get__dashboard__thumbnail.html": 4640268631621939949, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_parse_dates.py": 5344929031479212951, - "backend/alembic/versions/8dd0a93af539_drop_deprecated_source_language_column_.py": 3574830605384611294, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/METADATA": 10241403930811743893, - "docs/api/html/group__SupersetDashboardsListMixin.html": 17489314668228618185, - "docs/api/html/group__frontend__src__lib__stores__sidebar__js____toggleMobileSidebar.html": 2764792336812485289, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/LICENSE": 16174281248426886206, - "backend/src/api/routes/__tests__/test_tasks_logs.py": 13245318244727271907, - "docs/api/html/group__preview__session__serializer.html": 5685665567268715831, - "docs/api/html/group__get__dictionary.html": 6783587429209921357, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_cli.py": 3173259257428855556, - "venv/lib/python3.13/site-packages/numpy.libs/libscipy_openblas64_-096271d3.so": 14338208504131470036, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/adapter.py": 5908944827805063108, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/WHEEL": 7454950858448014158, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_map.py": 1115471561925426082, - "venv/lib/python3.13/site-packages/pip/_internal/commands/help.py": 4175797322922912326, - "docs/api/html/group__is__multimodal__model.html": 3951793497188229823, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/nattype.pyi": 1830368009811266890, - "venv/lib/python3.13/site-packages/pandas/io/formats/templates/string.tpl": 15796203549130590784, - "docs/api/html/group__OverrideLanguageRequest.html": 1786154171429944136, - "docs/api/html/group__PromoteResponse.html": 6455148757266042889, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_elffile.py": 5728213398707817049, - "docs/api/html/group__updateUploadCategory__Function.html": 17493381273334242237, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_indexing.py": 17106010337164216722, - "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/__init__.py": 4761936067832638585, - "venv/lib/python3.13/site-packages/pandas/compat/numpy/function.py": 18405870240333383757, - "venv/lib/python3.13/site-packages/pandas/core/indexes/base.py": 496956059910401121, - "venv/lib/python3.13/site-packages/httpx/py.typed": 15130871412783076140, - "docs/api/html/group__test__no__end__time__skipped.html": 6670304755300596026, - "docs/api/html/group__run__enterprise__clean__e2e.html": 6563961649768557058, - "docs/api/html/group__build__stage__run__record.html": 374732547729464177, - "docs/api/html/group__test__already__processed__event.html": 11093851268510393622, - "venv/lib/python3.13/site-packages/pip/_internal/req/__init__.py": 12266077985828139928, - "docs/api/html/group__DatasetReviewEntryUxPageTests.html": 12544802803136468314, - "docs/api/html/graph_legend.dot": 5298420021651416491, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ndarray_misc.pyi": 16219573290116396099, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timezones.py": 1962830142478494009, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/__init__.py": 6532094538918910232, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/annotation.py": 15855576199737688124, - "docs/api/html/group__DashboardHub__normalizeValidationStatus__Function.html": 6303647604098249484, - "backend/src/services/clean_release/report_builder.py": 14118825155100404762, - "venv/lib/python3.13/site-packages/pygments/lexers/varnish.py": 3608370156692616959, - "docs/api/html/group__TasksSessionLocal.html": 16634222622624891127, - "docs/api/html/globals_func_j.html": 9168488517116821616, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/ccalendar.cpython-313-x86_64-linux-gnu.so": 11019871270094058038, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/licenses/LICENSE": 14230156892574098522, - "docs/api/html/group__ValidationPolicy.html": 17809048165198520903, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_constructors.py": 1697610790604400105, - "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/scheduler.py": 13650497976959209825, - "docs/api/html/group____dict__to__response.html": 16049941736309608274, - "venv/lib/python3.13/site-packages/pygments/lexers/functional.py": 15896849233007178031, - "venv/lib/python3.13/site-packages/uvicorn/importer.py": 5449629250971153435, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/websockets/server.py": 16599051743322571790, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/sessions.py": 5400174351499383072, - "docs/api/html/group__ensure__initial__admin__user.html": 9396064717758252856, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filter.py": 16846063190466267618, - "venv/lib/python3.13/site-packages/referencing/tests/test_retrieval.py": 10782087416909177703, - "venv/lib/python3.13/site-packages/_pytest/recwarn.py": 15312759837838083596, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/markers.py": 8297667990263004097, - "venv/lib/python3.13/site-packages/smmap/mman.py": 9091562788592406032, - "docs/api/html/group__test__get__dashboards__profile__filter__matches__owner__object__payload__contract.html": 13048004686797783316, - "venv/lib/python3.13/site-packages/fastapi/openapi/constants.py": 1111838716953068285, - "docs/api/html/group__test__extract__native__filters__from__key__dict__value.html": 1412362209295645370, - "venv/lib/python3.13/site-packages/numpy/_utils/_pep440.py": 15854257948148516490, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex_like.py": 1407890639206728014, - "docs/api/html/group____build__expected__columns.html": 382226139122586930, - "docs/api/html/group__TemplateVariableDto.html": 25487742519419104, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/precision.f90": 5518911932373565642, - "venv/lib/python3.13/site-packages/pyasn1/codec/cer/__init__.py": 15728752901274520502, - "docs/api/html/group__ResourceType.html": 6653107601280859808, - "docs/api/html/group__frontend__e2e__helpers__api__helper__js____getToken.html": 2202102914736726657, - "venv/lib/python3.13/site-packages/pydantic/v1/parse.py": 5875950480538559709, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numeric.py": 14451517189908259173, - "backend/src/plugins/translate/__tests__/test_dictionary_crud.py": 2458404726842277862, - "docs/api/html/group__isDatasetReviewConflictError__Function.html": 9798021926177492027, - "docs/api/html/group__test__sync__environment__deletes__stale__mappings.html": 11101146135939187286, - "venv/lib/python3.13/site-packages/cryptography-46.0.3.dist-info/licenses/LICENSE.APACHE": 8359973023624924624, - "docs/api/html/group__TaskContextModule.html": 16641711360891107456, - "frontend/src/lib/ui/Icon.svelte": 18238812161544525261, - "venv/lib/python3.13/site-packages/requests/__version__.py": 6677532709758546816, - "venv/lib/python3.13/site-packages/numpy.libs/libquadmath-96973f99-934c22de.so.0.0.0": 5098713761946118139, - "docs/api/html/group__test__get__settings.html": 228135717549408768, - "backend/git_repos/remote/test-repo.git/objects/5d/9eb555c4cac2d3ffdba84c07682b154fe4180d": 10756202165146702691, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7009/__init__.py": 1923528231486706442, - "frontend/build/_app/immutable/nodes/19.Bja4SzOq.js": 6377685339228217285, - "backend/git_repos/remote/test-repo.git/objects/79/b070ad8af7bbc12169f7660d2a7a72ec7a889a": 8153595714065675791, - "docs/api/html/group__CleanReleaseRepository.html": 12202309648342354068, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__DictionaryBatchFilter.html": 14716813921994661678, - "venv/lib/python3.13/site-packages/rapidfuzz/utils.pyi": 7400725037332728015, - "venv/lib/python3.13/site-packages/h11/_abnf.py": 10456996134895429201, - "docs/api/html/group__AppConfig.html": 11574004031912899139, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/LICENSE": 6581019367004699816, - "docs/api/html/group__MaintenanceSettings.html": 7078307981035215602, - "docs/api/html/group__docker__backend__entrypoint__install__certificates.html": 18122978905545820697, - "backend/src/api/routes/translate/_correction_routes.py": 9348235443422223333, - "docs/api/html/group__test__get__all__metrics.html": 16314403640662640562, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7516/jwe.py": 10478268710546063196, - "docs/api/html/group__ComplianceExecutionResult.html": 11417659871930501440, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/challenge.py": 6388445104761482746, - "docs/api/html/navtreeindex16.js": 883027576628231514, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/linalg.pyi": 7217817172953767893, - "venv/lib/python3.13/site-packages/websockets/legacy/protocol.py": 762692033188392424, - "docs/api/html/group__ReportsRouter.html": 16841333188170208308, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/vector.py": 9941185556118343263, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-np0-objarr.npy": 1615546603475261360, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/METADATA": 3786116428839780263, - "frontend/playwright-report/data/288dba8d36913a1ca2ebbe6ef75a4997ea72bb65.png": 4602930255871194331, - "docs/api/html/group__RemoteRepoSchema.html": 11973800120981189119, - "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_polynomial.py": 777287409151500309, - "docs/api/html/group__GenerateDocsRequest.html": 10436232842092071054, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_cumulative.py": 15644124412739892167, - "frontend/playwright-report/trace/index.BCnMPevh.js": 8625484717968740943, - "docs/api/html/group__resume__task__with__password.html": 16159660988139123334, - "docs/api/html/group__get__datasource__columns.html": 14552107166235009597, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_insert.py": 6970975909565157982, - "docs/api/html/group__AssistantDatasetReview.html": 12183015005739052037, - "venv/lib/python3.13/site-packages/uvicorn/middleware/asgi2.py": 5355343704425405009, - "docs/api/html/group__toasts__Data.html": 4779111389094535460, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/terminal_theme.py": 13727399800263579809, - "venv/lib/python3.13/site-packages/pandas/_libs/arrays.pyi": 12629806207276576877, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_compat.py": 4677414058216516380, - "backend/src/api/routes/assistant/_llm_planner.py": 7325833916307204752, - "venv/lib/python3.13/site-packages/PIL/McIdasImagePlugin.py": 14137545069906812251, - "venv/lib/python3.13/site-packages/passlib/tests/_test_bad_register.py": 5446848310854472376, - "docs/api/html/group____make__user.html": 10705276908709984170, - "docs/api/html/group__OrchestratorHelpers.html": 15580278811762324793, - "venv/lib/python3.13/site-packages/referencing/exceptions.py": 2535322334430110414, - "backend/src/scripts/seed_permissions.py": 2299443594185631497, - "venv/lib/python3.13/site-packages/urllib3/util/__init__.py": 2848870790459558599, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/__init__.py": 8924316361489544973, - "venv/lib/python3.13/site-packages/numpy/matlib.pyi": 8017902013455539981, - "docs/api/html/group__test__create__task__missing__fields.html": 14663889192866601882, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/exceptions.py": 2873060848310644552, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_masked.py": 5380551301916549925, - "venv/lib/python3.13/site-packages/PIL/JpegPresets.py": 12342262642420923471, - "venv/lib/python3.13/site-packages/pip/_internal/network/lazy_wheel.py": 12936988990566680659, - "docs/api/html/group__test__update__session__surfaces__commit__time__session__version__conflict__payload.html": 11776750589639859419, - "venv/lib/python3.13/site-packages/greenlet/tests/fail_slp_switch.py": 16490710972475154509, - "docs/api/html/group__t__Store.html": 5003719811547922783, - "docs/api/html/group__EventBus.html": 1131138632704451045, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/errors.py": 12777931960088171310, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reorder_levels.py": 10996225584998355824, - "backend/src/services/profile_service.py": 7050751066987031905, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/__init__.py": 1856412417337875350, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_astype.py": 8505605634958454768, - "docs/api/html/group__AssistantAuditRecord.html": 15859560198913853671, - "venv/lib/python3.13/site-packages/pygments/styles/coffee.py": 17584414468786822371, - "docs/api/html/group__test__tui__f5__dispatches__run__action.html": 799267684182155396, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/session.py": 5126234270435358377, - "docs/api/html/group__docker__frontend__entrypoint__install__ca__certificates.html": 8734321705788517662, - "backend/git_repos/remote/test-repo.git/objects/d8/3fe0db1279ef07c4c4f3de4156a629373b4057": 7507193819578792576, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_npy_units.py": 8966226962799105383, - "docs/api/html/group____build__session__version__conflict__http__exception.html": 16116500469408421341, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_concatenate_chunks.py": 9693420220003544490, - "frontend/src/components/EnvSelector.svelte": 6483243153689240649, - "docs/api/html/group__getPermissions__Function.html": 4129456418206064840, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_regression.py": 5652583095949741238, - "venv/lib/python3.13/site-packages/requests/models.py": 9104898985870652256, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/errors.py": 4817746830102488448, - "venv/lib/python3.13/site-packages/websockets/asyncio/async_timeout.py": 7267209982032642028, - "venv/lib/python3.13/site-packages/jaraco/classes/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/anyio/pytest_plugin.py": 4165280709813718099, - "docs/api/html/group__DictionaryImportResult.html": 10670594679605704236, - "docs/api/html/group__DistributionManifest.html": 4125661865258730357, - "venv/lib/python3.13/site-packages/pyasn1/codec/streaming.py": 13202773554072937140, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.py": 5933622200264572885, - "docs/api/html/group__test__get__repository__status__batch__marks__item__as__error__on__service__failure.html": 15089064352958594879, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_localize.py": 14495969595744172955, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/type_api.py": 10299750173867399533, - "venv/lib/python3.13/site-packages/uvicorn/middleware/wsgi.py": 9179913252056394958, - "venv/lib/python3.13/site-packages/gitdb/test/test_pack.py": 7518388297247535189, - "venv/lib/python3.13/site-packages/pygments/lexers/fortran.py": 6859638690245583968, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/fromnumeric.py": 3792079757208879362, - "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.pyi": 18290764713161282980, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_delitem.py": 14336697139511118920, - "venv/lib/python3.13/site-packages/passlib/utils/md4.py": 4521366665754877351, - "venv/lib/python3.13/site-packages/numpy/_core/_struct_ufunc_tests.cpython-313-x86_64-linux-gnu.so": 13269324887178340397, - "venv/lib/python3.13/site-packages/pandas/core/computation/ops.py": 18250656489649699560, - "docs/api/html/group__GitServiceSyncMixin.html": 5716850320253869040, - "docs/api/html/group__create__job.html": 8027737551110382669, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/RECORD": 12575893084587070195, - "frontend/src/routes/+layout.ts": 8392799000856778740, - "docs/api/html/group__test__async__network__404__mapping__translates__dashboard__endpoints.html": 11153828192989017659, - "docs/api/html/group__DEFAULT__MAX__OUTPUT__TOKENS.html": 7465573242087549666, - "venv/lib/python3.13/site-packages/pip/_internal/cli/parser.py": 1064209424435927674, - "venv/lib/python3.13/site-packages/pandas/core/strings/__init__.py": 13025387151657797130, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_unicode.py": 1103608932321322989, - "docs/api/html/group__DictionaryManager.html": 10525609506852001360, - "venv/lib/python3.13/site-packages/uvicorn/loops/auto.py": 13086005977087619545, - "docs/api/html/group__test__get__repository__diff__propagates__http__exception.html": 8311817278452456051, - "docs/api/html/group__promote__dashboard.html": 8826303047033817087, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_to_numpy.py": 14845850776625255794, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_boxplot_method.py": 7635131059308651529, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_string.py": 15542851215119662814, - "docs/api/html/group__currentPage__Data.html": 5033996641622029546, - "frontend/build/_app/immutable/entry/start.PcCZoI2W.js": 2074623351869032930, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____endAllEvents.html": 3018938932417409550, - "docs/api/html/group____build__preference__response.html": 9896456427595935858, - "frontend/src/lib/stores/datasetReviewSession.js": 12759266804307584180, - "frontend/build/_app/immutable/chunks/D2LDFDH5.js": 1513005501777757570, - "venv/lib/python3.13/site-packages/_pytest/fixtures.py": 1442767529726551116, - "docs/api/html/group__test__extract__native__filters__from__key__single__filter.html": 17303628301397310552, - "docs/api/html/group__TestDictionaryUtils.html": 2864150937588414580, - "venv/lib/python3.13/site-packages/pandas/core/methods/describe.py": 17006836623161521092, - "backend/src/core/migration/archive_parser.py": 16510017196974398795, - "docs/api/html/group__LlmSettings.html": 17767977673670132111, - "docs/api/html/group__test__new__key__only__mode.html": 7530821276868419489, - "venv/lib/python3.13/site-packages/attrs/converters.py": 13109003765349201809, - "docs/api/html/group__RiskAssessorModule.html": 17900668792000979877, - "venv/lib/python3.13/site-packages/referencing/tests/test_jsonschema.py": 9815500901482570619, - "docs/api/html/group__LoggerModule.html": 7322757663720393423, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npy": 17303060773267383537, - "docs/api/html/group__GitDeps.html": 16624578576304918458, - "docs/api/html/group__DatasetHub.html": 15956774378542048676, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/__init__.py": 371829636800613137, - "venv/lib/python3.13/site-packages/httpx/_transports/__init__.py": 5931944625631942279, - "docs/api/html/group__NetworkModule.html": 8343794053715962544, - "venv/lib/python3.13/site-packages/pip/_internal/commands/wheel.py": 16397273591245583356, - "docs/api/html/group__ApiKeyCreateRequest.html": 14624348983729327471, - "docs/api/html/group__PreviewStatus.html": 7092182377354477188, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/diff.py": 5930189593946259160, - "backend/src/core/config_manager.py": 9235726125641848917, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_reshape.py": 5572426834735088916, - "docs/api/html/group__description.html": 1734080948702838188, - "docs/api/html/group__call__openai__compatible.html": 5568369277413456833, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_orm_types.py": 5508682532707462217, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/manifest.py": 16998683210177680416, - "backend/src/core/auth/logger.py": 14366097284329186897, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/__init__.py": 15130871412783076140, - "docs/api/html/group__setAssistantConversationId__Function.html": 7465015029614231837, - "docs/api/html/group__TestSupersetProfileLookup.html": 8558198565478717735, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated": 11193150813314469394, - "venv/lib/python3.13/site-packages/numpy/strings/__init__.pyi": 6213864891045042045, - "backend/src/plugins/llm_analysis/models.py": 7637744212927477541, - "docs/api/html/group__DashboardHub__getSortIndicator__Function.html": 2447268847448159799, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_misc.py": 16393451200513733144, - "docs/api/html/group__test__sync__network__404__mapping__translates__dashboard__endpoints.html": 1260303454387268885, - "venv/lib/python3.13/site-packages/pandas/core/internals/__init__.py": 1377108808392195313, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/RECORD": 11541505360973709787, - "docs/api/html/group__TestDictionaryLegacyHub.html": 9545067935919622524, - "venv/lib/python3.13/site-packages/pandas/core/ops/common.py": 17314999825118928100, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/bazaar.py": 5630511062945201327, - "venv/lib/python3.13/site-packages/numpy/_core/function_base.py": 5676611841071163854, - "venv/lib/python3.13/site-packages/pandas/tseries/offsets.py": 13750652921710295115, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/generic.py": 17755579497690180620, - "venv/lib/python3.13/site-packages/sqlalchemy/event/base.py": 18238453132776154057, - "venv/lib/python3.13/site-packages/pygments/lexers/_googlesql_builtins.py": 12251289884604745945, - "docs/api/html/group__StaticFiles.html": 11218723552719220007, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.cpython-313-x86_64-linux-gnu.so": 6011605826065363427, - "frontend/src/routes/storage/repos/+page.svelte": 2273592022555593164, - "backend/src/api/routes/assistant/_resolvers.py": 5636628127798453486, - "backend/src/plugins/translate/_token_budget.py": 13508548690925198562, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py": 12184194085993665945, - "docs/api/html/group__BuildManifestRequest.html": 4763470685395243475, - "docs/api/html/group__test__cli__release__gate__commands__scaffold.html": 4903992815060946212, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/padding.py": 7470483648602195268, - "venv/lib/python3.13/site-packages/numpy/rec/__init__.py": 7467092880535460658, - "backend/src/schemas/dataset_review_pkg/_dtos.py": 10114042767789130927, - "docs/api/html/group____make__config__manager.html": 7005648573888953729, - "venv/lib/python3.13/site-packages/numpy/linalg/__init__.py": 15647756307608142577, - "venv/lib/python3.13/site-packages/pandas/io/excel/_odswriter.py": 17220892094904423676, - "venv/lib/python3.13/site-packages/authlib/deprecate.py": 3563885283090710443, - "backend/src/core/task_manager/lifecycle.py": 1696300129247014205, - "docs/api/html/group__FindingSeverity.html": 10603170265602472688, - "docs/api/html/group__MaintenanceOrchestrators.html": 18097597528748465937, - "venv/lib/python3.13/site-packages/numpy/linalg/tests/test_linalg.py": 2939739913218718132, - "docs/api/html/globals_q.html": 11227781208567450336, - "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/random/__init__.pxd": 7355772669172093814, - "venv/lib/python3.13/site-packages/requests-2.32.5.dist-info/RECORD": 9678861487447993169, - "docs/api/html/group__LogEntry.html": 3138617379009952451, - "docs/api/html/group__TaskManagerPackage.html": 14112772634468690362, - "venv/lib/python3.13/site-packages/numpy/random/_examples/cffi/extending.py": 9859383893070793914, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_formats.py": 18080627205081311773, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/plugin.py": 16042078006552521613, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_highlight.py": 7573043358244876807, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/dml.py": 5340326603935367534, - "docs/api/html/group__DATABASE__URL.html": 863413084908346717, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs7.pyi": 2777456956260371820, - "venv/lib/python3.13/site-packages/pydantic/mypy.py": 10034028441785588819, - "venv/lib/python3.13/site-packages/pydantic/_internal/_model_construction.py": 694471028676746110, - "frontend/src/components/git/GitManager.svelte": 13371816170978247457, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/test_freq_code.py": 13758353172962109140, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.pyi": 4432290176776357346, - "docs/api/html/topics.js": 6653823426024009041, - "backend/src/api/routes/datasets.py": 3167720733470487965, - "docs/api/html/group__DashboardHub__getGitSummaryLabel__Function.html": 2221712068669890122, - "docs/api/html/group__get__llm__status.html": 4661440239824290304, - "venv/lib/python3.13/site-packages/pygments/lexers/berry.py": 4540479522141160472, - "docs/api/html/search/groups_17.js": 7938088704885798457, - "docs/api/html/group__LLMAnalysisScheduler.html": 3957615184107530467, - "venv/lib/python3.13/site-packages/pycparser/c_ast.py": 5942107079119742, - "backend/git_repos/remote/test-repo.git/objects/00/fcf77cecdf261aef91eda792ab3384a3339922": 1527330822006618217, - "backend/src/services/git_service.py": 12429116089116885657, - "docs/api/html/group__fetch__datasource__metadata.html": 12758308588265556383, - "docs/api/html/group__test__git__service__init__repo__reclones__when__path__is__not__a__git__repo.html": 3825554148900324930, - "docs/api/html/group__test__lookup__superset__accounts__env__not__found.html": 13190978675041730982, - "venv/lib/python3.13/site-packages/numpy/_typing/_scalars.py": 17701938912160369190, - "docs/api/html/group____resolve__provider__id.html": 8297130391733338866, - "venv/lib/python3.13/site-packages/pygments/lexers/capnproto.py": 15713281507099411738, - "venv/lib/python3.13/site-packages/authlib/consts.py": 14226604841596736870, - "venv/lib/python3.13/site-packages/smmap/test/test_buf.py": 6910367739329016619, - "venv/lib/python3.13/site-packages/httpcore/_sync/__init__.py": 7676937747094511752, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_accessor.py": 10074496835594855786, - "venv/lib/python3.13/site-packages/pandas/core/arrays/period.py": 11560984428402318984, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/reshape/api.py": 16364136812252343076, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_partial.py": 15129217814262096441, - "docs/api/html/group__getAssistantConversations__Function.html": 7365056083389809503, - "docs/api/html/group__get__all__plugin__configs.html": 14923346516628235508, - "frontend/build/_app/immutable/nodes/36.CGo72rZZ.js": 9214701916561283615, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py": 8530403239168529668, - "frontend/build/_app/immutable/chunks/Bz99hFgy.js": 2788010001753617987, - "backend/src/plugins/translate/preview_executor.py": 11680052270975606699, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/licenses/LICENSE": 1126906383149772681, - "docs/api/html/group__TaskLoggerModule.html": 11856387495799127523, - "docs/api/html/group__test__belief__scope__error__handling.html": 17275160222893974318, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arraypad.pyi": 12178766981280242620, - "docs/api/html/group__DEFAULT__LLM__PROMPTS.html": 7412466426050075586, - "docs/api/html/group__cot__log__function.html": 609040572561257151, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_polynomial.pyi": 9134949577567498894, - "venv/lib/python3.13/site-packages/pandas/_libs/lib.pyi": 3049004444338566197, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_markdown.py": 9964642152916165317, - "venv/lib/python3.13/site-packages/pygments/formatters/groff.py": 6480830447984073574, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/asyncio.py": 11442130378027628520, - "docs/api/html/group__update__git__config.html": 5484250484604366906, - "venv/lib/python3.13/site-packages/keyring/core.py": 2764269590123131634, - "venv/lib/python3.13/site-packages/numpy/_core/tests/_natype.py": 12013743717306900860, - "docs/api/html/group__get__datasets.html": 5086430945264562862, - "backend/src/services/git/_base.py": 18009686862342149948, - "backend/src/services/clean_release/facade.py": 4588791439430238149, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/assignOnlyModule.f90": 15967199601254912096, - "docs/api/html/group__get__app__timezone.html": 1707039123202823020, - "backend/src/core/database.py": 13971080974850727511, - "docs/api/html/globals_func_r.html": 4828713620959168383, - "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/RECORD": 4487477737768669516, - "venv/lib/python3.13/site-packages/pygments/lexers/typoscript.py": 6706632522710582762, - "venv/lib/python3.13/site-packages/dateutil/tz/tz.py": 5258008267965780817, - "docs/api/html/search/groups_5.js": 8198735349335934773, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_matmul.py": 16169282225324014901, - "venv/lib/python3.13/site-packages/pip/_internal/pyproject.py": 6155748929233245415, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/certs.py": 13366788049827102313, - "docs/api/html/group__test__storage__settings__endpoint__shape.html": 7615304938869259368, - "venv/lib/python3.13/site-packages/pygments/lexers/ml.py": 7510453371068494792, - "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5.js": 14348227999396735008, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_utils.pyi": 12312677132342982866, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_utils.pyi": 3708905060001289515, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_common.f": 5913807803369559749, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/__init__.py": 15130871412783076140, - "docs/api/html/group____project__dashboard__response__items.html": 4840220165425219906, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_selectable_constructors.py": 84919308502240880, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_multiplier.f": 13432803601367981068, - "venv/lib/python3.13/site-packages/pandas/tests/generic/__init__.py": 15130871412783076140, - "docs/api/html/group__SupersetContextTemplatesMixin____extract__primary__jinja__identifier.html": 13395638216145831396, - "docs/api/html/group__TestDictionaryCRUD.html": 909187372871752623, - "backend/src/models/git.py": 9387561498910534118, - "venv/lib/python3.13/site-packages/authlib/common/security.py": 2291133227104016684, - "backend/src/models/maintenance.py": 7247376865754977732, - "docs/api/html/group__sync__permission__catalog.html": 4976963130726233994, - "docs/api/html/group__test__delete__task__with__runs__flag.html": 3543661419042224408, - "docs/api/html/group____record__session__event.html": 3602405687107437058, - "venv/lib/python3.13/site-packages/pip/_internal/utils/misc.py": 17461379236189029253, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/datasource.pyi": 80441145804744162, - "frontend/build/_app/immutable/assets/9.tn0RQdqM.css": 15130871412783076140, - "backend/src/services/clean_release/repositories/artifact_repository.py": 7507938934134519998, - "venv/lib/python3.13/site-packages/pandas/core/reshape/merge.py": 300205528811693975, - "docs/api/html/group__TaskReport.html": 7520791941174149444, - "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/METADATA": 13695483537390128881, - "venv/lib/python3.13/site-packages/rsa/pem.py": 3118524021576082054, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/token.py": 4161760290367932793, - "venv/lib/python3.13/site-packages/numpy/lib/_ufunclike_impl.pyi": 9854069702366900663, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/on.py": 2866195356090164296, - "docs/api/html/group__sidebar__Store.html": 2096791117586109991, - "docs/api/html/group__frontend__src__services__git__utils__js____stageBadgeClass.html": 563611682311771515, - "docs/api/html/group__ExecutionMappingReview.html": 13336104515329815272, - "venv/lib/python3.13/site-packages/pandas/tests/io/test_html.py": 12040484325988635032, - "docs/api/html/group____update__confirmation__state.html": 4308473508764835316, - "venv/lib/python3.13/site-packages/pandas/tests/generic/test_generic.py": 1297407268239966429, - "docs/api/html/group__navigateUp__Function.html": 14835818415678519228, - "backend/git_repos/remote/test-repo.git/refs/heads/feature/test": 7714224145564125988, - "docs/api/html/group__authStore__Store.html": 7614884214374261421, - "docs/api/html/group__BatchApproveSemanticRequest.html": 5571958815851650821, - "docs/api/html/group__SidebarNavigationTest__Module.html": 17269318974786788900, - "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.pyi": 15509415708991012734, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_pairwise.py": 2673370322448160842, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/roles.py": 5448455900674232810, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/__init__.py": 9997757256282720103, - "venv/lib/python3.13/site-packages/packaging/py.typed": 15130871412783076140, - "docs/api/html/group__test__reject__then__publish__is__blocked.html": 6737257066366586136, - "venv/lib/python3.13/site-packages/PIL/_imagingcms.cpython-313-x86_64-linux-gnu.so": 15198265718775753589, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/sphinxext.py": 2403717774564798878, - "docs/api/html/group__SupersetClientAuthenticate.html": 14826114222268403337, - "venv/lib/python3.13/site-packages/cryptography/fernet.py": 10137792148038140630, - "docs/api/html/group__test__patch__dashboard__metadata__replaces__dataset__ids.html": 1839137190249919532, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/util/compat.py": 16148553252010089561, - "venv/lib/python3.13/site-packages/passlib/crypto/des.py": 14699826058580193499, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__datasource.py": 1747755531265478106, - "venv/lib/python3.13/site-packages/keyring/backends/Windows.py": 4103830951042463184, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop.py": 15751236355035730554, - "venv/lib/python3.13/site-packages/keyring/util/__init__.py": 9635016046220094473, - "frontend/build/favicon.svg": 6451919037497541980, - "docs/api/html/group__get__users__page.html": 9763104845120484996, - "docs/api/html/group__TranslationEventLog.html": 8646269919159445552, - "docs/api/html/group__ViolationCategory.html": 7515224565172006703, - "venv/lib/python3.13/site-packages/apscheduler/triggers/cron/expressions.py": 8015786996064244656, - "venv/lib/python3.13/site-packages/ecdsa/util.py": 10543788809724099209, - "docs/api/html/group__ensure__banner__chart.html": 16144864453932921383, - "frontend/src/lib/ui/PageHeader.svelte": 16740944781208407079, - "docs/api/html/group____find__dashboard__id__by__slug.html": 4034569905177392135, - "venv/lib/python3.13/site-packages/gitdb/db/__init__.py": 2615504918450276693, - "docs/api/html/group__test__start__end__time__before__start.html": 18093529765348516001, - "docs/api/html/group__DashboardHub__getFilterOptions__Function.html": 17463321849622099527, - "backend/src/api/__init__.py": 5287505122015708040, - "docs/api/html/group__push__changes.html": 8985338630049484293, - "docs/api/html/group__DebugToolPage.html": 15312548458590735605, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/validator.py": 11486125578281617670, - "venv/lib/python3.13/site-packages/pydantic/v1/generics.py": 1150594223414883339, - "docs/api/html/group__DeploymentEnvironmentSchema.html": 3378454771345026670, - "backend/src/core/auth/config.py": 205969007784846720, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/__init__.py": 15735839022111230173, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/util.py": 4815334059917765852, - "docs/api/html/group__TestMigrationEngine.html": 2873879812846856614, - "venv/lib/python3.13/site-packages/pygments/lexers/dylan.py": 16246299896150112270, - "venv/lib/python3.13/site-packages/authlib/integrations/sqla_oauth2/client_mixin.py": 17980753662107479779, - "docs/api/html/group__SupersetCompatibilityMatrixTests.html": 12258937098540314692, - "docs/api/html/group__editEnv__Function.html": 10218410065986469503, - "docs/api/html/group__test__get__dashboard__thumbnail__success.html": 9479616945914668390, - "docs/api/html/group__DEFAULT__EXECUTION__PROMPT__TEMPLATE.html": 4059085208364710234, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/memmap.pyi": 15629663272474054909, - "venv/lib/python3.13/site-packages/pandas/core/generic.py": 10174952306641057447, - "venv/lib/python3.13/site-packages/pandas/util/_exceptions.py": 14426860079366867683, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_getitem.py": 8414677531950073373, - "docs/api/html/group__TestPolicyResolutionService.html": 11614920422741434476, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_convert.py": 13349243029437881949, - "docs/api/html/group__test__mixed__active__and__completed.html": 13263869066783777040, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/licenses/COPYING": 15270149301471737292, - "backend/git_repos/remote/test-repo.git/HEAD": 12394318939628232491, - "docs/api/html/group__profile__preference__service.html": 11536607414856501254, - "venv/lib/python3.13/site-packages/sqlalchemy/pool/__init__.py": 4337679908168515343, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/version.py": 14723293207188512766, - "docs/api/html/group__merge__spec.html": 1755088978549667893, - "venv/lib/python3.13/site-packages/authlib/oidc/registration/__init__.py": 9448552677419908109, - "venv/lib/python3.13/site-packages/pandas/io/json/_table_schema.py": 3103776773020058805, - "venv/lib/python3.13/site-packages/pandas/tseries/api.py": 14785127136180503704, - "venv/lib/python3.13/site-packages/PIL/ImageTransform.py": 7825940907094338808, - "venv/lib/python3.13/site-packages/uvicorn/_compat.py": 2593000889619875759, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/redis.py": 11313520438600819390, - "venv/lib/python3.13/site-packages/greenlet/tests/leakcheck.py": 10179165748538820557, - "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.pyi": 1942236362568669510, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/tzconversion.pyi": 10947021928755741366, - "frontend/e2e/run-e2e.sh": 8106714116467101372, - "docs/api/html/group__TestTranslateSchedulerExecution.html": 685214232569505268, - "docs/api/html/group__handleBulkDelete__Function.html": 2943774690774667491, - "backend/tests/test_smoke_plugins.py": 8608573120580623257, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/highlighter.py": 10615901258492570740, - "venv/lib/python3.13/site-packages/numpy/lib/_version.pyi": 10804755840730584157, - "docs/api/html/group__ReleaseCandidate.html": 17443285489751037523, - "venv/lib/python3.13/site-packages/fastapi/staticfiles.py": 4094330072433302614, - "venv/lib/python3.13/site-packages/psycopg2/pool.py": 8855184580138330938, - "venv/lib/python3.13/site-packages/pandas/_config/dates.py": 3863381688916127265, - "docs/api/html/group__MaintenanceFailedDashboard.html": 17647303195778429767, - "docs/api/html/group__MappingModels.html": 5347446187914937942, - "venv/lib/python3.13/site-packages/starlette/concurrency.py": 9835859079437525910, - "venv/lib/python3.13/site-packages/pygments/lexers/ezhil.py": 10294520768762308887, - "backend/tests/test_storage_config.py": 14773544217703363468, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/filters/__init__.py": 835924536855006596, - "docs/api/html/group__loadRepositoryStatuses__Function.html": 16252993742945396544, - "docs/api/html/group__DEFAULT__CONTEXT__WINDOW.html": 14468316197072801713, - "docs/api/html/group____serialize__run__context.html": 12922073506719689996, - "docs/api/html/group__MappedFields.html": 12079507314876337159, - "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__RoleUpdate.html": 5923007426428235445, - "docs/api/html/group____FakeSession.html": 16348083183205532802, - "docs/api/html/group__ROUTES__DIR.html": 11229403533330790472, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npz": 9481735723557962988, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_protocols.py": 5019018645804313814, - "venv/lib/python3.13/site-packages/dateutil/tz/__init__.py": 9617254832690762236, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/scrypt.py": 15294662527620994511, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_equals.py": 9903250467264848187, - "docs/api/html/group__test__capabilities__question__returns__successful__help.html": 13279217848598726597, - "venv/lib/python3.13/site-packages/pyasn1/type/univ.py": 7163437624197683039, - "docs/api/html/group__test__get__dialect__from__database.html": 18388157090569434819, - "docs/api/html/group__TermCorrectionSubmit.html": 15931221897139852522, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_xlsxwriter.py": 8445547874832015203, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/testing.pyi": 9317922890783549326, - "docs/api/html/group__ViolationSeverity.html": 6547432338145120005, - "frontend/src/lib/components/dataset-review/ExecutionMappingReview.svelte": 17593699210431624897, - "venv/lib/python3.13/site-packages/anyio/_backends/__init__.py": 15130871412783076140, - "docs/api/html/group__get__assistant__audit.html": 12289421674798192339, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/__init__.py": 17630019785411546070, - "venv/lib/python3.13/site-packages/_pytest/compat.py": 10046464101087327473, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/box.py": 14303526978791498662, - "docs/api/html/group__test__root__path__is__string.html": 16492802671590347100, - "backend/src/plugins/translate/orchestrator.py": 6762154654706349158, - "docs/api/html/group____get__profile__filter__binding.html": 4542513203441105089, - "venv/lib/python3.13/site-packages/numpy/_core/getlimits.pyi": 15746478550640231553, - "docs/api/html/group__MigrationStatus.html": 182423064358330923, - "venv/lib/python3.13/site-packages/PIL/_imagingtk.pyi": 18222325750818585549, - "docs/api/html/group__MapperPlugin.html": 5002883678765248833, - "docs/api/html/group__CleanReleaseCliScript.html": 9859420981765754979, - "docs/api/html/group__TranslatePlugin.html": 3645253746992049971, - "docs/api/html/group__test__connection.html": 1381382283167612811, - "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.pyi": 6723628093359375823, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/hybrid.py": 5793056975404534989, - "venv/lib/python3.13/site-packages/charset_normalizer/legacy.py": 1706599349563920562, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_alpha_unix.h": 2759424570606117942, - "docs/api/html/group__ActivityTestModule.html": 550457699042373901, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/__init__.py": 16150188643076000414, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/plugin.py": 18006138132596504101, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_tz_convert.py": 77283525137564255, - "venv/lib/python3.13/site-packages/pygments/lexers/_sourcemod_builtins.py": 11809503654078535014, - "docs/api/html/group__ApprovalService.html": 5375853220886231078, - "docs/api/html/group__list__files.html": 13944554477611206786, - "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____getSessionUiPhase.html": 8539042222924950819, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/dtype.pyi": 465999539608816848, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_hashtable.py": 7762405492052106717, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py": 6067836157632573182, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/METADATA": 15987199384245264478, - "venv/lib/python3.13/site-packages/greenlet/tests/test_generator.py": 14156412320760444199, - "venv/lib/python3.13/site-packages/jsonschema-4.25.1.dist-info/METADATA": 4954909612926293404, - "docs/api/html/group__UserDashboardPreference.html": 6859297743426015504, - "docs/api/html/group__create__markdown__chart.html": 4622906128741204576, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_compound.f90": 15012692484070385863, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexing.py": 5910217172351020811, - "docs/api/html/group__runTask__Function.html": 7728857557116934185, - "venv/lib/python3.13/site-packages/PIL/BmpImagePlugin.py": 16682567630621529903, - "docs/api/html/group__build__security__summary.html": 9447535612374879505, - "frontend/src/lib/stores/__tests__/mocks/environment.js": 3065694655373335793, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_invalid.py": 2762344062554575805, - "venv/lib/python3.13/site-packages/ecdsa/_rwlock.py": 7971972922423146201, - "venv/lib/python3.13/site-packages/uvicorn/workers.py": 7213290478118197047, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval_range.py": 9900908262700749861, - "docs/api/html/group__WebSocketLogHandler______init____.html": 6496043986001513744, - "venv/lib/python3.13/site-packages/attr/_make.py": 2348458829123990579, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/cli/hiworld.f90": 6321872653169140461, - "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/extension_types.py": 1856757457657310231, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/conftest.py": 13388202260795998613, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/hooks.py": 8580161128344266243, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/mod.pyi": 10591120373577657317, - "venv/lib/python3.13/site-packages/numpy/core/_dtype.py": 2806449741844148191, - "venv/lib/python3.13/site-packages/pydantic/color.py": 1007422551673129827, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_where.py": 16584772339591698754, - "venv/lib/python3.13/site-packages/ecdsa/test_curves.py": 6577762025900435885, - "examples/maintenance-api-bash.sh": 674396824183793987, - "docs/api/html/group__build__partial__recovery__findings.html": 14829932052677515671, - "docs/api/html/group__BranchCreate.html": 4283325077168876664, - "backend/src/core/__tests__/test_throttled_scheduler.py": 3526995607557121417, - "venv/lib/python3.13/site-packages/pygments/lexers/pascal.py": 5992422120590352714, - "docs/api/html/group__AuthSecurityModule.html": 3598351476195431872, - "docs/api/html/group__ClarificationStateResponse.html": 8085920956673403769, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/parameters.py": 247312908510013930, - "venv/lib/python3.13/site-packages/dotenv/main.py": 7408889670575271484, - "venv/lib/python3.13/site-packages/pydantic/v1/annotated_types.py": 12925825196690299531, - "docs/api/html/group__build__default__stages.html": 1764973272728108457, - "backend/src/api/routes/translate/_run_list_routes.py": 11620203305409913352, - "venv/include/site/python3.13/greenlet/greenlet.h": 16216120538647882082, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arrow_interface.py": 5231449831607036031, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_groupby.py": 12709199432838648700, - "venv/lib/python3.13/site-packages/pygments/formatters/latex.py": 7514533384468139434, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/common_with_division.f": 13517753924061311074, - "docs/api/html/group__test__get__merge__status__returns__service__payload.html": 5423320726769507497, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/immutabledict.pxd": 13413785818999584986, - "docs/api/html/group__ApprovalDecision.html": 2897407194180760694, - "docs/api/html/group____validate__environment.html": 5948694607401903979, - "docs/api/html/group__ImportArtifactsRequest.html": 9466947930245217230, - "venv/lib/python3.13/site-packages/pip/_internal/vcs/versioncontrol.py": 13742159245132241245, - "docs/api/html/group__test__transform__zip__invalid__path.html": 6265988318849736978, - "venv/lib/python3.13/site-packages/numpy/_core/numeric.py": 12261531924304979741, - "venv/lib/python3.13/site-packages/numpy/_typing/_nested_sequence.py": 7827259347937485408, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_color.py": 3700828402084646308, - "frontend/src/assets/svelte.svg": 13872699371039797074, - "docs/api/html/group__frontend__src__services__adminService__js____createRole.html": 8909533477901199154, - "docs/api/html/group__reload__validation__policy.html": 6168739793611833472, - "docs/api/html/group__AdaptiveBatchSizer.html": 3807050557665810980, - "venv/lib/python3.13/site-packages/keyring/backends/libsecret.py": 11621088556661567306, - "docs/api/html/group__test__is__multimodal__model__emits__warning.html": 11946987825238879999, - "backend/src/api/routes/assistant/_dataset_review_dispatch.py": 555136946286235411, - "docs/api/html/group__test__persist__task__new.html": 15843661225530940640, - "backend/src/services/dataset_review/event_logger.py": 8944052747593678117, - "dist/docker/backend.0.1.0.tar.xz": 3591917885965226179, - "docs/api/html/group__delete__session.html": 5324305490692827144, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/util.py": 387985965709546512, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_formats.py": 2743326139283991615, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_argsort.py": 7320661835159648143, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi": 14119032813912827271, - "backend/src/plugins/translate/__tests__/test_preview.py": 13201708414970983394, - "docs/api/html/group__TestAPIKeyModel.html": 8065083023674038153, - "venv/lib/python3.13/site-packages/pygments/formatters/irc.py": 2133711525226123259, - "backend/src/api/routes/assistant/_command_parser.py": 4551109710541212154, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/conftest.py": 9673372914396625461, - "docs/api/html/group__API__Routes.html": 3741949103355825147, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/generate_umath_validation_data.cpp": 10880681943327843928, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_isocalendar.py": 5704842983090753914, - "frontend/src/lib/utils/debounce.js": 11611676161433422398, - "docs/api/html/group__SupersetUserProjection.html": 4661710962417506510, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_month.py": 14597470286884431085, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/licenses/COPYING": 14459782230785388765, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_combine.py": 16193618152415684421, - "venv/lib/python3.13/site-packages/cffi/ffiplatform.py": 6782148110863549472, - "docs/api/html/group__GitHelpers.html": 3500975807832633271, - "venv/lib/python3.13/site-packages/numpy/lib/scimath.py": 7616377937400918724, - "venv/lib/python3.13/site-packages/pandas/core/ops/invalid.py": 10186546927135277579, - "docs/api/html/group__ExecutionMappingDto.html": 13993327359271368811, - "docs/api/html/group__run__checks.html": 17159795815287064770, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_tzconversion.py": 7205566353454584259, - "venv/lib/python3.13/site-packages/pygments/lexers/numbair.py": 17382646010399794697, - "docs/api/html/group__test__filters__existing__keys.html": 12141627945650887732, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_astype.py": 4854809596041987380, - "docs/api/html/group__ReportBuilder.html": 16300601000239313366, - "venv/lib/python3.13/site-packages/pandas/io/parquet.py": 14415622950469421518, - "docs/api/html/group__test__get__dashboard__tasks__history__sorting.html": 2186049035541654238, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_attr_equal.py": 5612259834202470413, - "docs/api/html/group____validate__provider.html": 10145375276233333598, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/filepost.py": 16698380375728063923, - "docs/api/html/group__MaintenanceBannerRenderer.html": 6099946816052023316, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_common.py": 9097104320321411408, - "docs/api/html/group__EffectiveProfileFilter.html": 17635576406887586988, - "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/dotenv.py": 5872848911499749500, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/scalar_string.f90": 15018631706178918578, - "docs/api/html/search/mag_sel.svg": 3857167165871619873, - "docs/api/html/group__Imports__Block.html": 3093618662848079957, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_setitem.py": 15731267129055947469, - "backend/src/plugins/translate/preview_session_serializer.py": 8901687711861433007, - "backend/git_repos/remote/test-repo.git/refs/heads/main": 7667767390913789840, - "docs/api/html/group__test__invalid__key__raises__401.html": 17548308712736303847, - "venv/lib/python3.13/site-packages/fastapi/logger.py": 11590581556317053213, - "docs/api/html/group__test__prior__run__no__records__returns__all.html": 18180477270004399232, - "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.pyi": 14069586330277925998, - "docs/api/html/group__AnswerKind.html": 4721759660402979766, - "docs/api/html/group__test__migrate__dashboards__success.html": 13646594525073241233, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/modules.py": 9023244176010175562, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/mod.py": 2054876501774773382, - "docs/api/html/search/functions_d.js": 5419224988785173409, - "venv/lib/python3.13/site-packages/authlib/__init__.py": 8957417237987377675, - "docs/api/html/group__MappingStatus.html": 18314626760542529454, - "docs/api/html/group____resolve__repo__key__from__ref.html": 12707845054172015148, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/discovery.py": 12479996594187326798, - "venv/lib/python3.13/site-packages/dateutil/zoneinfo/__init__.py": 10261654903106416888, - "docs/api/html/group__UserBase.html": 6482510430149113904, - "venv/lib/python3.13/site-packages/_pytest/helpconfig.py": 3033477806668470951, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dlpack.py": 7908102652282922952, - "docs/api/html/group__test__load__schedules__reloads__translation__schedules.html": 16059484179450134258, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/__init__.py": 8434616194114282088, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/provision.py": 13156893203444936309, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/strings.pyi": 402863478592154500, - "venv/lib/python3.13/site-packages/gitdb/db/base.py": 3375449584942578385, - "docs/api/html/group__RunSourceFetcher.html": 14682302550003967669, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_monotonic.py": 14648437502004398715, - "venv/lib/python3.13/site-packages/httpx/_transports/mock.py": 13564349939188292363, - "venv/lib/python3.13/site-packages/cffi/vengine_gen.py": 6582900613549620556, - "venv/lib/python3.13/site-packages/pydantic/_internal/_mock_val_ser.py": 12265320534388472158, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_ops.py": 3875181889032080505, - "docs/api/html/tab_s.png": 9983007352471920092, - "venv/lib/python3.13/site-packages/pyasn1/type/char.py": 16518487114777185541, - "venv/lib/python3.13/site-packages/pygments/lexers/roboconf.py": 1335271183806115470, - "docs/api/html/group__FilterStateModels.html": 10931585648189564325, - "docs/api/html/group__DictionaryCRUD.html": 6797497851497770225, - "docs/api/html/group__SupersetClientGetDatasets.html": 9914093133934169605, - "docs/api/html/group__test__parse__datetime__datetime__object.html": 6514218695236425243, - "venv/lib/python3.13/site-packages/pandas/core/resample.py": 13814298871925341617, - "docs/api/html/group__SupersetCompilationAdapter__imports.html": 7332800023400610712, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_complex/foo77.f": 7353666717928699883, - "docs/api/html/group__CoreUtils.html": 3246108816835089471, - "docs/api/html/group__updateMapping__Function.html": 16524716365093515524, - "docs/api/html/group__ExportArtifact.html": 1743575886963594890, - "docs/api/html/globals_e.html": 5635107513608756533, - "docs/api/html/group__MappingMethod.html": 7292386745422091497, - "venv/lib/python3.13/site-packages/fastapi/security/http.py": 8465565118555441408, - "venv/lib/python3.13/site-packages/authlib/oauth2/auth.py": 8455258275234626534, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/apps.py": 4219758952914498116, - "docs/api/html/group__export__validation.html": 13641774792475599780, - "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.c": 2519219289444051739, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/conftest.py": 17514079438846557943, - "venv/lib/python3.13/site-packages/pygments/lexers/wren.py": 6259586056260713984, - "frontend/e2e/tests/live-project-check.e2e.js": 4657915737640702974, - "backend/src/services/clean_release/stages/manifest_consistency.py": 5890428472425990952, - "docs/api/html/group____index__by__uuid.html": 12082993556480921250, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/oct_key.py": 11504245602045724, - "docs/api/html/group__CleanReleaseModels.html": 12685389086616523632, - "venv/lib/python3.13/site-packages/pip/_internal/cli/main_parser.py": 7942770969280622910, - "venv/lib/python3.13/site-packages/PIL/BdfFontFile.py": 10811640174253687845, - "backend/alembic/versions/a7b1c2d3e4f5_add_provider_id_to_validation_policies.py": 7446022200459040057, - "venv/lib/python3.13/site-packages/pluggy/_result.py": 16790448391766505769, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/__init__.py": 5118993772178969104, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getPrimaryActionCtaLabel.html": 14241540356676774140, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/py.typed": 9796674040111366709, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/fft.pyi": 14454280827038327842, - "docs/api/html/group__DocPreview.html": 10374942202918391563, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_file_buffer_url.py": 15377043158761455858, - "backend/src/services/maintenance/_dashboard_scanner.py": 1401108008906511280, - "backend/alembic_test.db": 14995253579408279707, - "venv/lib/python3.13/site-packages/pygments/styles/gh_dark.py": 7717523746712391809, - "docs/api/html/group__TranslationExecutionEngine.html": 6997454185504177526, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/ocsp.pyi": 8666343427862784832, - "docs/api/html/group__test__get__task__log__stats__success.html": 17411659170669298649, - "docs/api/html/group__test__add__logs__empty.html": 5564646933686414085, - "docs/api/html/group__SupersetClientModule.html": 204765713104060593, - "docs/api/html/group__MaintenanceDashboardBannerStatus.html": 4467400780797905740, - "venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py": 7899068668225389287, - "venv/lib/python3.13/site-packages/pycparser/_c_ast.cfg": 2649342580905806389, - "venv/lib/python3.13/site-packages/keyring/__init__.py": 991260689223167957, - "venv/lib/python3.13/site-packages/keyring/backends/fail.py": 5792496235026678021, - "venv/lib/python3.13/site-packages/authlib/integrations/starlette_client/integration.py": 3085827906490477329, - "docs/api/html/group__frontend__src__lib__api__validation__js____deleteTask.html": 15128947433545338265, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_exceptions.py": 15290185665769490169, - "venv/lib/python3.13/site-packages/numpy/_core/function_base.pyi": 8343632640078378312, - "docs/api/html/group__BackupPlugin.html": 16158251941328976430, - "venv/lib/python3.13/site-packages/pandas/core/tools/timedeltas.py": 7221393731712061943, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/merge/test_merge_cross.py": 17742712348672218852, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/LICENSE": 2032205056284080825, - "docs/api/html/group__test__create__valid.html": 7128826085418675137, - "docs/api/html/group__MetricDescriptionUpdate.html": 7274378175344562378, - "docs/api/html/group__normalize__user__payload.html": 7049600760546559055, - "venv/lib/python3.13/site-packages/numpy/lib/_arraypad_impl.pyi": 2186875333484796630, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py2-objarr.npy": 9694707264403109829, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_partial_indexing.py": 4132091154215910029, - "venv/lib/python3.13/site-packages/pydantic/plugin/_schema_validator.py": 857640644840976862, - "docs/api/html/group__FieldProvenance.html": 13226719866506423057, - "docs/api/html/globals_func_i.html": 8474102779220933859, - "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.py": 11393954913234866059, - "docs/api/html/group____run__task.html": 10393447256976591681, - "docs/api/html/group__name.html": 9476374294755334273, - "venv/lib/python3.13/site-packages/numpy/linalg/lapack_lite.cpython-313-x86_64-linux-gnu.so": 11074560904305737689, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_nonunique_indexes.py": 15468078561381939405, - "docs/api/html/search/files_0.js": 1537156212390677004, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_stride_tricks.py": 1064774375151677743, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_version.pyi": 18093228021414910594, - "backend/tests/test_datasets.py": 9766141609165223604, - "docs/api/html/group__test__get__dashboards__pagination__zero__page.html": 9231270374996443556, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/writeonly.py": 14213960431308620600, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/expression.py": 1248029135284283479, - "venv/lib/python3.13/site-packages/anyio-4.12.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/PIL/_imaging.cpython-313-x86_64-linux-gnu.so": 1425926264271197601, - "docs/api/html/group__fetchDashboards__Function.html": 15265666455140275558, - "docs/api/html/group__loadHistory__Function.html": 688973918186380673, - "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/RECORD": 15574202654585730126, - "docs/api/html/group____audit.html": 6003328311178572551, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_get_dummies.py": 14871264482622327003, - "docs/api/html/group__TranslationJobConfig.html": 15629227135566862060, - "backend/src/services/clean_release/repositories/compliance_repository.py": 2888085832526178946, - "venv/lib/python3.13/site-packages/PIL/ContainerIO.py": 7551929914241379005, - "docs/api/html/group__loadStatus__Function.html": 424560253324877602, - "docs/api/html/group__StorageIndexPage.html": 3670377713830788417, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/flatiter.pyi": 9598576095384661517, - "venv/lib/python3.13/site-packages/passlib/exc.py": 15559705487889657091, - "frontend/src/lib/components/reports/__tests__/reports_page.integration.test.js": 966219842574639412, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_numeric.py": 9417128508433693658, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_indexing.py": 4063662953863959087, - "frontend/playwright-report/trace/assets/codeMirrorModule-Ds_H_9Yq.js": 1369559823166915102, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____updateTranslationRunState.html": 9070808497640938645, - "docs/api/html/group__Environment.html": 9167040397257623124, - "docs/api/html/group__SupersetClientCompileDatasetPreview.html": 15872086106011031941, - "docs/api/html/group____log.html": 10162958821169656028, - "venv/lib/python3.13/site-packages/pandas/conftest.py": 747626550806435763, - "docs/api/html/group__DatasetsApi.html": 7357837522405925673, - "docs/api/html/group__i18n__Module.html": 10632521022228083958, - "venv/lib/python3.13/site-packages/PIL/ImageChops.py": 16368812693381154921, - "frontend/src/lib/components/reports/ReportsList.svelte": 17367781544989044334, - "frontend/build/_app/immutable/assets/AssistantChatPanel.D4L5jlt0.css": 10724144138366384732, - "docs/api/html/group__launch__dataset.html": 8280088988730100751, - "docs/api/html/group____sync__raw__payload__from__config.html": 1731463979685810569, - "docs/api/html/group__BackupTypes__Module.html": 6128924600139653510, - "venv/lib/python3.13/site-packages/authlib/common/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_reindex.py": 16348619731902304844, - "docs/api/html/group__test__delete__run__not__found.html": 14187887394068528264, - "docs/api/html/group__get__clarification__state.html": 5572805696675612675, - "docs/api/html/group__LLMReportPage.html": 1528192812986476498, - "docs/api/html/group__SingleConfigDbMock.html": 1705315936176408905, - "docs/api/html/group__test__fetchPreviewRecords__Function.html": 3636775598407887299, - "venv/lib/python3.13/site-packages/_pytest/config/compat.py": 13175516717549204627, - "venv/lib/python3.13/site-packages/psycopg2/errorcodes.py": 6711830404954314463, - "venv/lib/python3.13/site-packages/pandas/core/config_init.py": 16537122488227845090, - "frontend/src/routes/tools/storage/+page.svelte": 8817697925409987305, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group____transform__yaml.html": 11756968780316598035, - "docs/api/html/group__SupersetAccountLookupResponse.html": 3758644365858317477, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_floats.py": 3064615530495290792, - "docs/api/html/group__belief__scope__Function.html": 12952448433922544231, - "docs/api/html/group__MaintenanceRoutesModule.html": 17831095570734372209, - "frontend/src/routes/maintenance/+page.svelte": 5467232163447861441, - "docs/api/html/group____require__execution__flag.html": 5795997050372262927, - "docs/api/html/group__test__empty__events.html": 13036218116544520363, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py": 16768579082462294745, - "docs/api/html/group__GitRepoRoutes.html": 10793486578721949471, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/ranges/test_join.py": 11910166566689889389, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_holiday.py": 8541197358003711845, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/off.py": 16254524872582859561, - "venv/lib/python3.13/site-packages/attr/filters.py": 14156141488477438777, - "docs/api/html/group__get__migration__settings.html": 13897329278259758640, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_map.py": 32086858815359344, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/style.py": 12654125988995362593, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_ops.py": 16346030307660048773, - "backend/src/services/clean_release/__tests__/test_source_isolation.py": 462142504664192849, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/resultproxy.pyx": 5760108632332714287, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23533.f": 12558972205518833024, - "docs/api/html/group__LiveProjectCheckE2E.html": 17814476116211572824, - "docs/api/html/group__BackupRequest.html": 9361740148201115669, - "docs/api/html/group____load__fixture.html": 803262356442504240, - "venv/lib/python3.13/site-packages/passlib/utils/handlers.py": 10182859005870689026, - "venv/lib/python3.13/site-packages/pandas/core/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pydantic/v1/mypy.py": 17076910404409672245, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_2_complexcompat.h": 5001035774408914877, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_as_unit.py": 8492383355919959393, - "docs/api/html/group__process__batch.html": 15294657492230512680, - "backend/git_repos/remote/test-repo.git/objects/6a/f2b28e314c04fb4e03476c12d6491213591512": 4526311953587849385, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py": 12775850858284584296, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling.py": 18221459425751198218, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/INSTALLER": 17282701611721059870, - "venv/bin/f2py": 11006035631504708368, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/take.py": 3170415198452493008, - "docs/api/html/group__test__publish__without__approval__rejected.html": 3363942287403935607, - "docs/api/html/group__test__init__falls__to__defaults.html": 6035220370511717887, - "backend/src/services/dataset_review/repositories/session_repository.py": 4897386456198762089, - "docs/api/html/search/groups_15.js": 2849271939807724756, - "venv/lib/python3.13/site-packages/numpy/random/_pickle.pyi": 16081120364372391381, - "venv/lib/python3.13/site-packages/pygments/lexers/installers.py": 5083244110640899732, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libpcre-9513aab5.so.1.2.0": 12298861477332583668, - "docs/api/html/group__ProfileValidationError.html": 8590349442789923081, - "docs/api/html/group__assistantChat__Store.html": 18317790308537053696, - "docs/api/html/group__DatasetReviewSessionStore.html": 16665757289211685601, - "venv/lib/python3.13/site-packages/pygments/lexers/matlab.py": 1880915927722177654, - "venv/lib/python3.13/site-packages/httpx/_transports/base.py": 6498990531443644896, - "docs/api/html/group__test__repository__typo__fixed.html": 4874005037242233206, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_string.py": 13234624995755549329, - "venv/lib/python3.13/site-packages/pydantic/v1/tools.py": 1995883197897112278, - "venv/lib/python3.13/site-packages/httpx/_types.py": 12461152436147308761, - "venv/lib/python3.13/site-packages/pandas/core/indexes/timedeltas.py": 13653393430363556843, - "venv/lib/python3.13/site-packages/rsa/pkcs1_v2.py": 3533398238491175229, - "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.py": 12886275429547303345, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/dtypes.py": 9839461501970023861, - "backend/src/core/utils/superset_context_extractor/_templates.py": 13945363390780279343, - "docs/api/html/group__TaskLogPersistenceService.html": 767164108519071447, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/queue.py": 7820997545569991670, - "venv/lib/python3.13/site-packages/pygments/lexers/igor.py": 6506221273085723501, - "frontend/src/components/Navbar.svelte": 2640465687592983765, - "backend/src/models/dataset_review_pkg/_clarification_models.py": 10650387708191883580, - "frontend/build/_app/immutable/nodes/30.BRycvjeE.js": 10674967404181978072, - "venv/lib/python3.13/site-packages/_pytest/assertion/__init__.py": 16392348866405910618, - "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/AUTHORS": 10073225185964928425, - "venv/lib/python3.13/site-packages/jose/backends/cryptography_backend.py": 2875270748627611635, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/resource_protector.py": 9147671710440653567, - "docs/api/html/group__test__create__gitea__pull__request__returns__branch__error__when__target__missing.html": 12846340033666855401, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_astype.py": 8633811117672837999, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/_openssl.pyi": 4376871621615688626, - "frontend/src/types/dashboard.ts": 9718666955722367566, - "docs/api/html/group__send__message.html": 13998342804671032700, - "venv/lib/python3.13/site-packages/pygments/lexers/webassembly.py": 17811970490091406867, - "docs/api/html/group__test__single__event.html": 10966926393815748610, - "venv/lib/python3.13/site-packages/pyasn1/compat/__init__.py": 5902203086150636670, - "backend/src/scripts/__init__.py": 5400372851656282343, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/ndarraytypes.h": 6765088137325865789, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_callback.py": 1689177048527329209, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta_range.py": 15081095442418142820, - "docs/api/html/globals_func_e.html": 5853577764297783560, - "venv/lib/python3.13/site-packages/pydantic/decorator.py": 6984214881365537811, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_array.py": 16867692337986161626, - "docs/api/html/group__TranslationScheduleItem.html": 12108491525286964033, - "docs/api/html/group__setLoading__Function.html": 3715890994479237152, - "venv/lib/python3.13/site-packages/pandas/io/formats/info.py": 215303194287834907, - "docs/api/html/group__test__null__source__data__handled.html": 9321216567493581904, - "venv/lib/python3.13/site-packages/pygments/lexers/other.py": 6893672181003034910, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_constructors.pyi": 4993066609272697419, - "venv/lib/python3.13/site-packages/numpy/_core/_type_aliases.py": 1644743742545329282, - "docs/api/html/group__hash__api__key.html": 11201468271890098251, - "venv/lib/python3.13/site-packages/urllib3/util/ssl_match_hostname.py": 7411350247004265632, - "docs/api/html/group__initializeForm__Function.html": 9614464266538572602, - "venv/lib/python3.13/site-packages/_pytest/freeze_support.py": 494952714451267295, - "docs/api/html/group__create__user.html": 3710853753512075388, - "docs/api/html/search/functions_8.js": 3321647565030130819, - "backend/src/plugins/translate/orchestrator_exec.py": 965301343860940811, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_categorical.py": 1329115818264527103, - "docs/api/html/group__test__trigger__validation__skips__missing__policy.html": 5602615003618981152, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/query.py": 3867868366067025348, - "venv/lib/python3.13/site-packages/apscheduler/triggers/calendarinterval.py": 12481650996882675949, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop_duplicates.py": 2397396876872403048, - "venv/lib/python3.13/site-packages/pygments/lexers/gleam.py": 5161116162065925222, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/response.py": 15004896908941411698, - "venv/lib/python3.13/site-packages/pygments/styles/paraiso_light.py": 15941591873059208999, - "backend/src/api/routes/__tests__/test_profile_api.py": 10215800611882169470, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/reduce.py": 14976712507854054776, - "venv/lib/python3.13/site-packages/pygments/lexers/modula2.py": 6575895980250662093, - "venv/lib/python3.13/site-packages/pygments/lexers/_luau_builtins.py": 13690910306072616818, - "backend/src/core/task_manager/persistence.py": 9599258324212845325, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_transpose.py": 1686867045869336370, - "docs/api/html/group__test__get__dashboards__empty.html": 10251761497684394331, - "venv/lib/python3.13/site-packages/pandas/core/internals/concat.py": 12151675567885981464, - "venv/lib/python3.13/site-packages/anyio/abc/_tasks.py": 16839170934610571514, - "venv/lib/python3.13/site-packages/anyio/abc/_resources.py": 8330687082933028623, - "venv/lib/python3.13/site-packages/jeepney/tests/test_wrappers.py": 4813328996575597980, - "venv/lib/python3.13/site-packages/pandas/core/arrays/datetimes.py": 10585034798097337148, - "frontend/src/lib/stores/environmentContext.js": 9960741825828562980, - "backend/src/models/assistant.py": 13943482616669949278, - "docs/api/html/navtreedata.js": 10541563969521895995, - "venv/lib/python3.13/site-packages/ecdsa/eddsa.py": 15966858338456571506, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_recfunctions.py": 16014503284544747867, - "venv/lib/python3.13/site-packages/numpy/lib/tests/data/win64python2.npy": 11005857803794805939, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/issue232.py": 6498198296113395367, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/attrs/filters.py": 6844006230924162452, - "venv/lib/python3.13/site-packages/numpy/dtypes.py": 14128421398870718692, - "frontend/src/lib/ui/Button.svelte": 14815370250842191735, - "docs/api/html/group____normalize__object__payload.html": 14403994834116059510, - "venv/lib/python3.13/site-packages/websockets/asyncio/client.py": 5694225754072131767, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ed448.pyi": 13016654355866474644, - "backend/src/services/sql_table_extractor.py": 11429348671339263380, - "docs/api/html/group__GitDashboardPage.html": 4862973845020805849, - "docs/api/html/group__test__storage__config__fields__have__correct__types.html": 13803318122009828230, - "docs/api/html/group__SettingsE2E.html": 1243339859806566484, - "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.py": 15558318749939680074, - "docs/api/html/group____resolve__target__envs.html": 326825327677354212, - "docs/api/html/group__GitOperationsPanel.html": 14155571809658908034, - "docs/api/html/group__NetworkError______init____.html": 13550401008734324366, - "frontend/src/lib/components/translate/TranslationPreview.svelte": 9514416214545061268, - "backend/src/plugins/translate/_batch_insert.py": 12482635271028646257, - "docs/api/html/group__HomePage.html": 10705890495789315886, - "docs/api/html/group__RootLayoutConfig__Module.html": 11134316622827325111, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/licenses/LICENSE": 6808958893921859106, - "docs/api/html/search/searchdata.js": 12335622014985822676, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/pymssql.py": 6650008663791598065, - "docs/api/html/group__network__error__handler.html": 11593637847095297203, - "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.py": 1809069379864985298, - "docs/api/html/group____build__risks.html": 10011809177370998034, - "backend/src/core/auth/__tests__/test_auth.py": 6524647391979341734, - "backend/tests/services/clean_release/test_report_audit_immutability.py": 4795829940402801626, - "venv/lib/python3.13/site-packages/pandas/core/window/online.py": 13551140636252302201, - "frontend/src/lib/components/translate/__tests__/test_bulk_replace_modal.svelte.js": 16164320505605430516, - "venv/lib/python3.13/site-packages/pygments/lexers/bqn.py": 11823648528816858738, - "venv/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_impl.py": 16610770935842519289, - "docs/api/html/group__test__list__active__schedules.html": 11816123497638243525, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_numba.py": 203614377324678234, - "frontend/e2e/e2e-nohealth.mjs": 5921562935931064381, - "docs/api/html/nav_hd.png": 5189739344381957223, - "docs/api/html/group__PreparationService.html": 8547646096904159114, - "venv/lib/python3.13/site-packages/greenlet/tests/test_cpp.py": 10982107081267457286, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token_validator.py": 2932866595454490527, - "docs/api/html/menu.js": 10605519113858156037, - "docs/api/html/group____parse__datetime.html": 16073684679023407314, - "venv/lib/python3.13/site-packages/pip/_internal/models/scheme.py": 4101322315594430874, - "venv/lib/python3.13/site-packages/_pytest/_code/__init__.py": 6211607282116942091, - "venv/lib/python3.13/site-packages/numpy/polynomial/chebyshev.py": 1590647503660777156, - "docs/api/html/group__test__delete__logs__for__tasks__empty.html": 11042736857003485976, - "venv/lib/python3.13/site-packages/numpy/_expired_attrs_2_0.py": 8786558894094307341, - "backend/src/plugins/translate/preview_prompt_builder.py": 15837150631111510351, - "venv/lib/python3.13/site-packages/rapidfuzz/_feature_detector_cpp.cpython-313-x86_64-linux-gnu.so": 16337157355618216738, - "venv/lib/python3.13/site-packages/websockets/asyncio/router.py": 15039562376314233669, - "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/py.typed": 15130871412783076140, - "docs/api/html/group__info.html": 3684644634418690403, - "venv/lib/python3.13/site-packages/numpy/lib/_user_array_impl.pyi": 7171810177801103765, - "venv/lib/python3.13/site-packages/jeepney/auth.py": 10565040329507087892, - "docs/api/html/group__AssistantApi.html": 466508613702940100, - "docs/api/html/group__NotificationChannel.html": 13421940193975834527, - "docs/api/html/group__ManifestService.html": 17042827860507544398, - "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py": 11917862648584782420, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/algorithms.py": 17106903014205241774, - "docs/api/html/group__getDatasetReviewConflictMessage__Function.html": 969336450247771573, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libsasl2-883649fd.so.3.0.0": 13306917506408577991, - "frontend/playwright-report/trace/defaultSettingsView.BDKsFU3c.css": 12935551588108679759, - "docs/api/html/group__SetupTestsModule.html": 5544696171487564620, - "venv/lib/python3.13/site-packages/ecdsa/test_der.py": 7117444285097824495, - "venv/lib/python3.13/site-packages/pandas/api/types/__init__.py": 3995030354191071797, - "venv/lib/python3.13/site-packages/git/objects/commit.py": 12317865358846331802, - "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.py": 9654828595008925976, - "docs/api/html/group__AdminApiKeyRoutes.html": 1930874666734065642, - "docs/api/html/group__TestCleanReleaseLegacyCompat.html": 15482783963436488047, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9207/parameter.py": 2793853093222900337, - "docs/api/html/group__updateUser__Function.html": 14752881444899728084, - "venv/lib/python3.13/site-packages/anyio/_core/_asyncio_selector_thread.py": 1760597191437520000, - "docs/api/html/group__JSON__OVERHEAD__PER__ROW.html": 5671854497093663857, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_exceptions.py": 12145393002202744314, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/compat.py": 9758194415584776667, - "docs/api/html/group__handleFilterChange__Function.html": 12314374793481426477, - "frontend/src/lib/components/layout/sidebarNavigation.js": 14306348671588504207, - "docs/api/html/group__TranslationRunProgress.html": 17186985638749776681, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccos.csv": 3026156288992132911, - "docs/api/html/group__AuthState__Interface.html": 8632817980324199273, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_reflection.py": 10520778598547003532, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/evaluator.py": 6348022789899831255, - "venv/lib/python3.13/site-packages/pandas/_libs/sas.pyi": 873957906443703977, - "docs/api/html/group__start__check.html": 10263972923109456955, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/kind/foo.f90": 1367964042730902899, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_util.py": 6573034224727513309, - "venv/lib/python3.13/site-packages/pandas/core/sorting.py": 689057022713741051, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arithmetic.py": 14858832264660410908, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/licenses/LICENSE": 3868190977070717994, - "venv/lib/python3.13/site-packages/keyring/util/platform_.py": 16978095707950463638, - "docs/api/html/group__get__health__summary.html": 12658858476431678972, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_transpose.py": 17983160984979323393, - "venv/lib/python3.13/site-packages/urllib3/connection.py": 12761371748350595804, - "venv/bin/py.test": 2593802493707545818, - "frontend/playwright.config.js": 802586619504012017, - "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/WHEEL": 17410883677306885019, - "venv/lib/python3.13/site-packages/sqlalchemy/connectors/asyncio.py": 4200928269459447593, - "docs/api/html/group__delete__tasks.html": 2265288733173423107, - "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename.py": 16148017223289654683, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/__init__.py": 11009955319062089477, - "docs/api/html/group__DeploymentEnvironment.html": 16685176550591536655, - "venv/lib/python3.13/site-packages/sqlalchemy/future/__init__.py": 17688444679736059779, - "docs/api/html/group__setup__method.html": 17742160113140228512, - "venv/lib/python3.13/site-packages/_pytest/legacypath.py": 7512065138259788426, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_einsum.py": 17286384318722237180, - "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.py": 1731362089136926666, - "venv/lib/python3.13/site-packages/pip/__main__.py": 574438877257676911, - "docs/api/html/group__test__validate__path__nonexistent__root.html": 15516438199424145609, - "venv/lib/python3.13/site-packages/pandas/core/nanops.py": 18291534943985310005, - "docs/api/html/group__get__remote__id.html": 4917003252379423579, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_function_base.py": 15241999259666702619, - "docs/api/html/group__AssistantLlmPlanner.html": 3353552519708970803, - "venv/lib/python3.13/site-packages/pillow.libs/libharfbuzz-0692f733.so.0.61230.0": 23110287346167671, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunclike.pyi": 14623308681600207918, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/stride_tricks.pyi": 8257977532799807847, - "venv/lib/python3.13/site-packages/numpy/f2py/f90mod_rules.py": 2953876934840155920, - "venv/lib/python3.13/site-packages/pygments/lexers/mojo.py": 8616283481592061285, - "docs/api/html/group__edit__dictionary__entry.html": 12444753978279908445, - "docs/api/html/group__NavigationMock__Module.html": 931761641099207828, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_astype.py": 11995587502880659624, - "docs/api/html/group__ui__route.html": 955048899493840648, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_subclass.py": 11096337912872573314, - "backend/src/api/routes/git/_helpers.py": 17217408354237437064, - "backend/tests/core/test_mapping_service.py": 8795874006773064566, - "docs/api/html/group__list__gitea__repositories.html": 5900420781022521601, - "venv/lib/python3.13/site-packages/jsonschema/tests/test_types.py": 651959450717569029, - "frontend/build/_app/immutable/nodes/3.CiFNdR4r.js": 6604637647308878596, - "backend/src/services/clean_release/demo_data_service.py": 14506634271910590558, - "venv/lib/python3.13/site-packages/numpy/lib/__init__.pyi": 1537495515469182226, - "docs/api/html/group__test__button__check__disabled__logic.html": 3976064615352650231, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/object/test_astype.py": 563689238486630866, - "docs/api/html/group____do__http__request.html": 5541890670779429458, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/errors.py": 17581857895562494222, - "venv/lib/python3.13/site-packages/authlib/oidc/core/claims.py": 2035724920423374302, - "venv/lib/python3.13/site-packages/httpx-0.28.1.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__get__payload.html": 3413926737425743336, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/licenses/LICENSE": 1424673738228762254, - "venv/lib/python3.13/site-packages/PIL/ImageEnhance.py": 3218405223202808764, - "venv/lib/python3.13/site-packages/pygments/styles/manni.py": 11853576691280294949, - "venv/lib/python3.13/site-packages/ecdsa/keys.py": 10281170640484982154, - "docs/api/html/group__TranslateJobUpdate.html": 3630855679853267875, - "venv/lib/python3.13/site-packages/more_itertools/recipes.py": 5254043689078173976, - "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__LLMAnalysisService.html": 4482613675136608683, - "venv/lib/python3.13/site-packages/pandas/tests/util/conftest.py": 11143177766735481660, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_rowcount.py": 6682800584070485197, - "venv/lib/python3.13/site-packages/pygments/lexers/c_cpp.py": 1211842877043321095, - "docs/api/html/group__test__tui__f5__run__smoke__reports__blocked__state.html": 5494023614389428629, - "venv/lib/python3.13/site-packages/websockets/sync/utils.py": 15135020989207191168, - "docs/api/html/group__DatasetList.html": 15827060935687053250, - "venv/lib/python3.13/site-packages/rapidfuzz/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/METADATA": 317417773265681256, - "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/rsa.py": 5392922995791487437, - "docs/api/html/group__test__transform__zip__end__to__end.html": 11478692460747852800, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_unique.py": 3148372351781648868, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/windows.py": 16452996247375488034, - "docs/api/html/search/functions_11.js": 18073114919847941486, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_setops.py": 15196593522537569500, - "docs/api/html/group__AsyncSupersetClientClose.html": 17574786361007292921, - "venv/lib/python3.13/site-packages/ecdsa/curves.py": 812969717084691115, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/ufuncs.py": 12732599294816239111, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/enumerated.py": 16623643153933348755, - "docs/api/html/group__MappingCreate.html": 12518001484805787826, - "docs/api/html/group__ReportsService.html": 2032865134635125460, - "docs/api/html/group__setAssistantFocusTarget__Function.html": 13485754571727078546, - "docs/api/html/group__DatasetReviewSchemaComposites.html": 187782560876687868, - "venv/lib/python3.13/site-packages/numpy/core/overrides.pyi": 1968648615494204783, - "docs/api/html/group__getReportDetail__Function.html": 15901398277426320441, - "docs/api/html/navtreeindex9.js": 16489008554418984705, - "docs/api/html/group__ADGroupMappingCreate.html": 332191690038373330, - "venv/lib/python3.13/site-packages/gitdb/test/test_example.py": 3039934065817753241, - "docs/api/html/group__test__features__from__env__still__applied.html": 5745023344665096858, - "docs/api/html/group__test__inactive__schedule__skips.html": 17459537030862916433, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/memmap.pyi": 2834143910417841603, - "docs/api/html/group__AssistantDatasetReviewDispatch.html": 15266638409291242280, - "docs/api/html/group____write__dashboard__yaml.html": 9223157474938909024, - "venv/lib/python3.13/site-packages/pydantic/error_wrappers.py": 11273827025986230629, - "venv/lib/python3.13/site-packages/pandas/_libs/window/aggregations.pyi": 6072607880689125113, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_offsets_properties.py": 2340780879864207500, - "venv/lib/python3.13/site-packages/httpcore/_backends/auto.py": 10488908050307887802, - "venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py": 13206297306768491360, - "venv/lib/python3.13/site-packages/cffi/_embedding.h": 9441285871823018603, - "docker/frontend.Dockerfile": 11291701046055590331, - "docs/api/html/group__get__features.html": 12928666550710504587, - "docs/api/html/group__DatasetRunContextDto.html": 17960720894742072910, - "venv/lib/python3.13/site-packages/passlib/tests/test_apache.py": 12515001998770565375, - "venv/lib/python3.13/site-packages/greenlet/platform/setup_switch_x64_masm.cmd": 8473866644286785815, - "docs/api/html/group__insert__banner__markdown__at__top.html": 2314550343350178265, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/twofactor/__init__.py": 18069619555394133945, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_defchararray.py": 15135904718007443306, - "venv/lib/python3.13/site-packages/pandas/core/arrays/boolean.py": 8821671562937866269, - "venv/lib/python3.13/site-packages/pygments/formatters/terminal.py": 14380579279194052030, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_chaining_and_caching.py": 15814259766479305290, - "venv/lib/python3.13/site-packages/pygments/lexers/_vbscript_builtins.py": 9882134558469491349, - "frontend/build/_app/immutable/nodes/26.sxWJsmff.js": 14493423158006980228, - "docs/api/html/globals_dup.js": 12007683462695373588, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/fromnumeric.pyi": 6708743411405679061, - "venv/lib/python3.13/site-packages/_pytest/terminal.py": 885450969703476729, - "venv/lib/python3.13/site-packages/pip/_internal/utils/appdirs.py": 539688259643447487, - "venv/lib/python3.13/site-packages/pip/_vendor/certifi/__init__.py": 2833016678144975576, - "docs/api/html/group__explore.html": 2270854509477383382, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ec.py": 8864112842559211971, - "venv/lib/python3.13/site-packages/pydantic/json_schema.py": 15678278224696353978, - "docs/api/html/group__TranslateRunsApi.html": 11590382264152453045, - "venv/lib/python3.13/site-packages/pydantic/v1/decorator.py": 6124748023704700724, - "venv/lib/python3.13/site-packages/pip/_internal/models/format_control.py": 12916176880659590456, - "docs/api/html/group__SqlLabLaunchPayload.html": 7835290887584234240, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_split_partition.py": 2715491071550545667, - "docs/api/html/group__get__environment__databases.html": 8137104331651503447, - "docs/api/html/group__SemanticSourceType.html": 8482820622466037543, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_manylinux.py": 12272275001961360101, - "venv/lib/python3.13/site-packages/referencing/__init__.py": 10093824711214801917, - "docs/api/html/group__test__list__jobs__with__status__filter.html": 5736540040640427487, - "venv/lib/python3.13/site-packages/pandas/core/groupby/indexing.py": 18009437216172136227, - "venv/lib/python3.13/site-packages/pillow.libs/libpng16-4a38ea05.so.16.53.0": 13136089038268964667, - "docs/api/html/group__PermissionDeniedError.html": 13284579335142541515, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/constants.pyi": 15633065380510755171, - "docs/api/html/search/all_5.js": 15854593946678342554, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/inout.f90": 1521999519583206538, - "docs/api/html/group__BatchInsertService.html": 3576868243265659369, - "docs/api/html/group__resume__clarification.html": 15214230648786619854, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24662.f90": 4241310200087270990, - "docs/api/html/group__Input.html": 4271267002431901392, - "backend/git_repos/remote/test-repo.git/hooks/applypatch-msg.sample": 12627299623252391515, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_assumed_shape.py": 18361009188431483164, - "docs/api/html/group__SMTPProvider.html": 4088022190794089412, - "venv/lib/python3.13/site-packages/pip/_internal/utils/temp_dir.py": 8657533190026387896, - "venv/lib/python3.13/site-packages/apscheduler/executors/tornado.py": 6635576580306890718, - "venv/lib/python3.13/site-packages/pandas/tests/test_flags.py": 411845322592830527, - "backend/src/core/task_manager/cleanup.py": 13007785075566443785, - "docs/api/html/group__PermissionSchema.html": 6474689152156131373, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_pandas.py": 10894229569442891394, - "docs/api/html/group__TestAPIKeyAuth.html": 9836588749523231088, - "venv/lib/python3.13/site-packages/pycparser/c_lexer.py": 10964496587169353564, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_methods.py": 12694335778731418517, - "docs/api/html/search/all_b.js": 1386014000926361523, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_free.f90": 10939657740734987454, - "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/numpy/polynomial/hermite.pyi": 3573629536236108391, - "venv/lib/python3.13/site-packages/rpds/__init__.py": 5862197328247012275, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_wrap.py": 17083394123203662880, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/requirements.py": 11478648568700804705, - "venv/lib/python3.13/site-packages/passlib/ext/django/utils.py": 8118896525895185626, - "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/METADATA": 2604612822968175379, - "frontend/src/lib/api/translate/schedules.js": 8616345664085016472, - "docs/api/html/group__list__dictionaries.html": 17979156195580437161, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_spinners.py": 15873416546130487469, - "venv/lib/python3.13/site-packages/authlib/jose/jwk.py": 13560749074922587626, - "docs/api/html/group__test__approve__rejects__foreign__report.html": 3963674165080312460, - "venv/lib/python3.13/site-packages/PIL/IcoImagePlugin.py": 14483964191652228364, - "docs/api/html/group__CotJsonFormatter__format.html": 824540483579618600, - "docs/api/html/group__InvalidZipFormatError.html": 13344492549849220399, - "venv/lib/python3.13/site-packages/websockets/asyncio/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_x64_msvc.h": 17581467060937454503, - "docs/api/html/group__frontend__src__lib__api__maintenance__js____startMaintenance.html": 15248104502781351783, - "frontend/build/_app/immutable/nodes/7.DtyspEKO.js": 1441856430291863908, - "frontend/build/_app/immutable/chunks/BtMNxLZr.js": 5550630982455761657, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/jaraco/classes/ancestry.py": 11742194518360056470, - "docs/api/html/group__test__superset__client__module__loads.html": 18034638000182478147, - "docs/api/html/group__delete__validation__policy.html": 7423748920813803796, - "venv/lib/python3.13/site-packages/itsdangerous/exc.py": 11181421963755661477, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_period.py": 7727606662611927293, - "docs/api/html/group__get__validation__policies.html": 18157978184513887167, - "venv/lib/python3.13/site-packages/secretstorage-3.5.0.dist-info/licenses/LICENSE": 6965457853328313914, - "docs/api/html/group__handleUpdate__Function.html": 5587686760218191810, - "venv/lib/python3.13/site-packages/git/util.py": 7424113599506269603, - "venv/lib/python3.13/site-packages/pygments/lexers/parsers.py": 12372594608244797844, - "venv/lib/python3.13/site-packages/pygments/lexers/apdlexer.py": 14222752852221009085, - "backend/src/services/dataset_review/repositories/__tests__/test_session_repository.py": 16488635694638612020, - "venv/lib/python3.13/site-packages/pygments/lexers/whiley.py": 218468511185731869, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/util.pyx": 649821942746469002, - "venv/lib/python3.13/site-packages/numpy/__init__.cython-30.pxd": 1476688171642957054, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/f2cmap/isoFortranEnvMap.f90": 15768643271492041853, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/index.py": 2763962019423787504, - "docs/api/html/group__NotificationService.html": 14374014839364668482, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/__init__.py": 14571251344510763303, - "venv/lib/python3.13/site-packages/click/testing.py": 15973659312284678327, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_resolution.py": 18198858926948468112, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/charset_normalizer/constant.py": 3070316197495651997, - "docs/api/html/group____build__tool__catalog.html": 15194959807685697743, - "docs/api/html/group__StageExecutionResult.html": 7320596610838714730, - "docs/api/html/group__SupersetClientExtractNativeFiltersFromPermalink.html": 11300039242994090403, - "venv/lib/python3.13/site-packages/greenlet/PyModule.cpp": 3464405102256525511, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/base.py": 11386927749822539057, - "docs/api/html/group__test__artifact__import__contract.html": 17628340742855449745, - "docs/api/html/group__get__translation__schedules.html": 13478548102530293059, - "docs/api/html/group__SupersetContextTemplatesMixin____collect__query__bearing__expressions.html": 17879749034024990070, - "docs/api/html/group__UpdateExecutionMappingRequest.html": 12143869237726717989, - "venv/lib/python3.13/site-packages/pandas/util/_tester.py": 1033951890304257539, - "venv/lib/python3.13/site-packages/pygments/lexers/__init__.py": 4627540245914849670, - "venv/lib/python3.13/site-packages/websockets/headers.py": 13587146967490088657, - "docs/api/html/group__download__skipped__csv.html": 13671264474809862196, - "docs/api/html/group____make__task__manager.html": 5653521975879492956, - "docs/api/html/group__update__banner__markdown__content.html": 5764762595078036751, - "dist/docker/frontend.e2e-test-20260526-112457.tar.xz": 9007148704121026255, - "docs/api/html/group__TranslationPlanner.html": 11482656897357674989, - "venv/lib/python3.13/site-packages/passlib/handlers/windows.py": 8969739861295277439, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/transforms.py": 3773159138960141434, - "venv/lib/python3.13/site-packages/pandas/tests/io/excel/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_libalgos.py": 13934861984871432400, - "venv/lib/python3.13/site-packages/urllib3/__init__.py": 1854778274753799815, - "docs/api/html/group__ThrottledSchedulerConfigurator.html": 4428654146760584758, - "venv/lib/python3.13/site-packages/click/_termui_impl.py": 3296085065310341855, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_select_dtypes.py": 3530442956913622572, - "docs/api/html/group__test__git__service__ensure__gitflow__branches__creates__and__pushes__missing__defaults.html": 831567849032163336, - "docs/api/html/group__GlobalSetup.html": 2598937001273501380, - "docs/api/html/group__test__get__database__mappings__env__not__found.html": 5367128710946773652, - "docs/api/html/group__HealthSummaryResponse.html": 15269424363939168020, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_openssl.py": 10446441599800965961, - "docs/api/html/group__decode__token.html": 4126673060024098894, - "venv/lib/python3.13/site-packages/_pytest/tracemalloc.py": 5530234062530858720, - "venv/lib/python3.13/site-packages/numpy/core/defchararray.py": 11371045148957547088, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/array_api_info.pyi": 7887489694871300469, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dtypes.py": 4985057130807802395, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_format.py": 16150524965028369071, - "venv/bin/pygmentize": 2166491742035772877, - "frontend/src/components/git/useGitManager.js": 2963542080215648548, - "venv/lib/python3.13/site-packages/PIL/PcdImagePlugin.py": 922809545463280224, - "venv/lib/python3.13/site-packages/sqlalchemy/event/api.py": 987321343916500337, - "docs/api/html/group__test__get__reports__filter__and__pagination.html": 15756423594624112678, - "docs/api/html/group____find__dashboard__id__by__slug__async.html": 17708878383316867946, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_serialization.py": 2011519400081029270, - "docs/api/html/group__test__json__load__if__needed__integer.html": 16897843606184491072, - "frontend/src/lib/components/layout/TaskDrawer.svelte": 1108434227611806299, - "docs/api/html/group____resolve__env__id.html": 6378272965191199149, - "venv/lib/python3.13/site-packages/pydantic/v1/dataclasses.py": 7833468513057242440, - "venv/lib/python3.13/site-packages/iniconfig-2.3.0.dist-info/METADATA": 16498441272591167134, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_dtypes.py": 7550022036887594386, - "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__retry__insert.html": 11772440759533263654, - "venv/lib/python3.13/site-packages/six.py": 948867418687428421, - "venv/lib/python3.13/site-packages/jsonschema/exceptions.py": 15651767069704734674, - "frontend/build/_app/immutable/assets/0.DK5emaTL.css": 4565146476878643834, - "venv/lib/python3.13/site-packages/PIL/IcnsImagePlugin.py": 1870235606309784691, - "backend/tests/test_task_manager.py": 4305202721942095979, - "docs/api/html/group__create__future.html": 6308958188691584151, - "docs/api/html/group__delete__health__report.html": 7248475607320545469, - "frontend/build/_app/immutable/chunks/Dn-Kr3zV.js": 11981784044886973393, - "docs/api/html/group__get__storage__settings.html": 15563164066742946450, - "docs/api/html/group____ensure__translation__schedules__columns.html": 7448712454629073660, - "venv/lib/python3.13/site-packages/httpcore/_synchronization.py": 10997080307461682786, - "venv/lib/python3.13/site-packages/passlib/handlers/md5_crypt.py": 7669328623914474137, - "venv/lib/python3.13/site-packages/numpy/_core/umath.py": 6132761780113682667, - "docs/api/html/group__list__dictionary__entries.html": 12208362720304738846, - "docs/api/html/group__DICT__TOKENS__MAX.html": 5949599154091293863, - "venv/lib/python3.13/site-packages/pandas/tests/test_multilevel.py": 10500316376521962061, - "venv/lib/python3.13/site-packages/h11/_events.py": 13800479928851716538, - "venv/lib/python3.13/site-packages/pip/_internal/operations/build/build_tracker.py": 9389444024234384408, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7521/client.py": 17305914656270781911, - "venv/lib/python3.13/site-packages/pip/_internal/cache.py": 210214424535803946, - "docs/api/html/navtreeindex19.js": 1947581984165981900, - "docs/api/html/group__FilterSource.html": 17708105643619200082, - "venv/lib/python3.13/site-packages/pydantic/v1/validators.py": 1060954771151908883, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/quoted_character/foo.f": 15318083678609435813, - "venv/lib/python3.13/site-packages/pygments/styles/_mapping.py": 11752660241391232497, - "venv/lib/python3.13/site-packages/pygments/lexers/carbon.py": 6753624244809529841, - "frontend/src/types/backup.ts": 1741234034896689348, - "docs/api/html/group__NotificationProvider.html": 9879076556154479190, - "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.py": 11621577915097515056, - "frontend/src/lib/stores/__tests__/test_activity.js": 13851679930048900037, - "docs/api/html/group____default__config.html": 6364950080332281281, - "venv/lib/python3.13/site-packages/requests/_internal_utils.py": 4091305333167213279, - "docs/api/html/group__ManifestRepository.html": 9159461313767192573, - "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____readTabFromUrl.html": 15117558677865436806, - "venv/lib/python3.13/site-packages/git/objects/submodule/base.py": 10140490301897269395, - "scripts/build_offline_docker_bundle.sh": 1393881756263325163, - "venv/lib/python3.13/site-packages/pygments/lexers/fift.py": 7537570342576781911, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/entities.py": 8001287806688585081, - "venv/lib/python3.13/site-packages/starlette/schemas.py": 697416731579555195, - "docs/api/html/group__handleDeleteEnv__Function.html": 14510731287987560462, - "venv/lib/python3.13/site-packages/pandas/_libs/join.cpython-313-x86_64-linux-gnu.so": 2947947484481830203, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/liblber-1c9097e2.so.2.0.200": 2651879839738900594, - "docs/api/html/group__MaintenanceDashboardStateStatus.html": 16864192271527139550, - "venv/lib/python3.13/site-packages/anyio/abc/_sockets.py": 15042495744291042062, - "venv/lib/python3.13/site-packages/attrs-25.4.0.dist-info/licenses/LICENSE": 8438117004702803716, - "venv/lib/python3.13/site-packages/charset_normalizer/__init__.py": 12412600648990392633, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/x448.py": 3960540415600165213, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqldb.py": 2138006249556140141, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_cython.py": 2933400021016349431, - "venv/lib/python3.13/site-packages/jsonschema/benchmarks/json_schema_test_suite.py": 6152158310190822762, - "venv/lib/python3.13/site-packages/numpy/random/_pickle.py": 7774620933480975313, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_isetitem.py": 14895552405828068177, - "venv/lib/python3.13/site-packages/numpy/typing/__init__.pyi": 10058149460938883683, - "backend/src/api/routes/dataset_review.py": 6955521302681155464, - "docs/api/html/group__count__remaining__questions.html": 777004046773548975, - "frontend/src/lib/components/dataset-review/SemanticLayerReview.svelte": 2835359394064716026, - "venv/lib/python3.13/site-packages/pandas/tests/test_sorting.py": 10751098438703044961, - "docs/api/html/group__CleanProfilePolicy.html": 16264819993053610464, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_where.py": 14167991754625133668, - "docs/api/html/plusd.svg": 5924202389722020174, - "docs/api/html/globals_func_a.html": 13712688806462460438, - "venv/lib/python3.13/site-packages/idna/codec.py": 4260241549062101779, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_loadtxt.py": 17562784940619942447, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_bool.py": 433342705586552936, - "frontend/svelte.config.js": 8473636210766821560, - "venv/lib/python3.13/site-packages/sqlalchemy/util/_py_collections.py": 16825222625411635946, - "docs/api/html/group__Button.html": 10473089147190690338, - "docs/api/html/group__get__run__history.html": 15431464627723325766, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/theme.py": 14225878259183479398, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_arithmetic.py": 976970179757115407, - "docs/api/html/group__service.html": 13599013976618262143, - "venv/lib/python3.13/site-packages/fastapi/security/open_id_connect_url.py": 3502444317759870079, - "docs/api/html/group__ValidationPolicyCreate.html": 15832204788479864944, - "venv/lib/python3.13/site-packages/pandas/core/computation/check.py": 16048939716341154196, - "docs/api/html/group__SupersetClientNormalizeEffectiveFiltersForQueryContext.html": 14678121546228653746, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/base.py": 10514193453447954780, - "docs/api/html/group__ProfileSettingsStateIntegrationTest.html": 16949360457251592220, - "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/RECORD": 9974324769263969827, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/WHEEL": 16784970174376303810, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_qcut.py": 8972418112914954758, - "docs/api/html/group__test__backup__dashboards__env__not__found.html": 8478433620760041140, - "docs/api/html/group__DashboardHub__toggleFilterDropdown__Function.html": 2486046615713604253, - "venv/lib/python3.13/site-packages/pandas/tests/base/test_constructors.py": 2439700578107653984, - "venv/lib/python3.13/site-packages/numpy/lib/mixins.pyi": 15597536290223628777, - "venv/lib/python3.13/site-packages/greenlet/TGreenletGlobals.cpp": 17400526028982558937, - "docs/api/html/doxygen__docs_8h_source.html": 6115156353195135130, - "docs/api/html/group__test__get__dashboards__profile__filter__matches__display__alias__without__detail__fanout.html": 15114828617044175570, - "venv/lib/python3.13/site-packages/anyio/streams/tls.py": 4191898645554875562, - "docs/api/html/group__BulkReplaceResponse.html": 9373003844931059590, - "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_config.py": 3636377988901907424, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/lower_f2py_fortran.f90": 13315882216322269383, - "venv/lib/python3.13/site-packages/pandas/core/groupby/categorical.py": 16588862198627920420, - "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____init.html": 6160087973904348720, - "docs/api/html/group__TestDictionaryCorrection.html": 17994177904345038747, - "docs/api/html/group__test__with__source.html": 12054958327519265597, - "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/RECORD": 11111103193315065053, - "docs/api/html/group____validate__superset__connection__fast.html": 1767320346178459487, - "docs/api/html/group__ManifestConsistencyStage.html": 12225398414320906138, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/intranges.py": 12536174834761591006, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_astype.py": 4975959187621745360, - "docs/api/html/group__SupersetClientExtractCompiledSqlFromPreviewResponse.html": 699068057352781572, - "docs/api/html/group__DEFAULT__PREVIEW__PROMPT__TEMPLATE.html": 4498507870595225234, - "backend/src/api/routes/git/_merge_routes.py": 12751818588668193353, - "docs/api/html/search/functions_9.js": 6423268791923178074, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_ddl.py": 3690189094298044728, - "venv/lib/python3.13/site-packages/pandas/_typing.py": 17795648994662434211, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_linux.h": 15772978805500070727, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/models.py": 6924198551176217533, - "venv/lib/python3.13/site-packages/pygments/lexers/stata.py": 7249891970174867363, - "backend/src/core/superset_client/_layout_utils.py": 1180616937544609564, - "docs/api/html/group__test__role__permission__association.html": 3634921328837162404, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/clsregistry.py": 1589692649837935355, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_cumulative.py": 16283954014170928273, - "venv/lib/python3.13/site-packages/PIL/PSDraw.py": 1927169421822446045, - "venv/lib/python3.13/site-packages/python_multipart-0.0.21.dist-info/RECORD": 7266315886702289726, - "venv/lib/python3.13/site-packages/pandas/tests/io/xml/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/_dml_constructors.py": 4497038002189188311, - "venv/lib/python3.13/site-packages/idna/uts46data.py": 5698404064763638591, - "venv/lib/python3.13/site-packages/passlib/handlers/mssql.py": 2800599582708170195, - "docs/api/html/group__test__full__mode__default.html": 3915571983694234061, - "docs/api/html/group__test__api__bulk__corrections.html": 13663959216271829060, - "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/mlib.ini": 16151597765994697599, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufuncs.pyi": 14995898537484305307, - "venv/lib/python3.13/site-packages/pip/_internal/utils/glibc.py": 12335359492756627313, - "docs/api/html/group__test__empty__input.html": 17644334770081506611, - "venv/lib/python3.13/site-packages/pygments/lexers/_julia_builtins.py": 8846111188795315103, - "venv/lib/python3.13/site-packages/pandas/core/ops/dispatch.py": 7041711677358147534, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/__init__.py": 15130871412783076140, - "backend/tests/services/clean_release/test_compliance_execution_service.py": 9658943217379916757, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE": 2345070715845618109, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_sample.py": 4641567828238191927, - "docs/api/html/group__SupersetContextParsingMixin.html": 6860207674910724587, - "docs/api/html/group__DashboardHub__getVisibleFilterOptions__Function.html": 17131127528619784346, - "venv/lib/python3.13/site-packages/greenlet/TThreadStateDestroy.cpp": 13749797532421836328, - "docs/api/html/group____ensure__user__dashboard__preferences__columns.html": 12772575671333756881, - "venv/lib/python3.13/site-packages/cryptography/hazmat/asn1/asn1.py": 8801155055979135092, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/base.py": 14908084548919567658, - "venv/lib/python3.13/site-packages/_pytest/cacheprovider.py": 5114545353355728201, - "venv/lib/python3.13/site-packages/pandas/core/arrays/_ranges.py": 2572903460327860111, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test__version.py": 9037916251087760255, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_construct_from_scalar.py": 16520136036631155842, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/mt19937-testset-1.csv": 16349400027062875699, - "backend/src/services/clean_release/compliance_orchestrator.py": 6747364300631405520, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_s390_unix.h": 5161826643658505126, - "docs/api/html/group__TestDeprecationAuditFixes.html": 9254534639082865942, - "docs/api/html/group__extractDatasetReviewVersion__Function.html": 18233279301174300230, - "venv/lib/python3.13/site-packages/pandas/core/ops/__init__.py": 4049432353810047365, - "docs/api/html/group__GitPackage.html": 8185926952400552290, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs12.pyi": 7801211423226951223, - "docs/api/html/group____extract__quoted__segment.html": 14177192301285608602, - "docs/api/html/group__list__plugins.html": 1922104558299502422, - "venv/lib/python3.13/site-packages/fastapi/security/__init__.py": 1345944646061334046, - "venv/lib/python3.13/site-packages/_pytest/pytester.py": 5423287282155948827, - "docs/api/html/search/functions_7.js": 7432376903275069025, - "docs/api/html/group__SupersetContextExtractor.html": 13588033302010707545, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/test_observance.py": 18178414966262524534, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/index_tricks.py": 10051261976498916957, - "docs/api/doxygen_stripped.c": 14291099198142844291, - "venv/lib/python3.13/site-packages/pandas/tests/extension/__init__.py": 15130871412783076140, - "frontend/src/routes/settings/LoggingSettings.svelte": 5172094246995863925, - "docs/api/html/group__TestCleanReleaseTui.html": 12992766318749624082, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_multithreading.py": 12547048606098186681, - "docs/api/html/group__TaskPersistenceService.html": 12977647248776427832, - "docs/api/html/group__ClarificationStatus.html": 10157970874585041006, - "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp_impl.cpython-313-x86_64-linux-gnu.so": 11767771662042404251, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.pyi": 13173581775000480811, - "venv/lib/python3.13/site-packages/pip/_vendor/pygments/token.py": 6987328478153445584, - "backend/src/plugins/translate/service_datasource.py": 10240814407336170041, - "docs/api/html/group__get__resource__mappings.html": 3377084676438392153, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sample.py": 16877779557071324115, - "docs/api/html/group__MergeResolveRequest.html": 17150347272995916362, - "docs/api/html/group__onMount__Function.html": 15261809730593069613, - "venv/lib/python3.13/site-packages/greenlet/__init__.py": 3447613556981242475, - "docs/api/html/group__test__end__all__active__events.html": 15618808237650024096, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth2_client.py": 6742098857947604729, - "venv/lib/python3.13/site-packages/httpx/_api.py": 6697370469329778523, - "venv/lib/python3.13/site-packages/starlette/middleware/authentication.py": 15211131264936212938, - "docs/api/html/globals_func.js": 13107479563907806427, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/__init__.py": 15130871412783076140, - "docs/api/html/group__handleCreateBackup__Function.html": 6719288563421799770, - "docs/api/html/group__test__resolve__namespace__separates__demo__and__real.html": 7744970425001491248, - "venv/lib/python3.13/site-packages/pydantic/_internal/_known_annotated_metadata.py": 9308620143218174987, - "docs/api/html/group__get__schedule.html": 11620587396014813935, - "venv/lib/python3.13/site-packages/pandas/core/col.py": 4221271461723119525, - "venv/lib/python3.13/site-packages/PIL/SunImagePlugin.py": 7661622930404688367, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/__init__.py": 11850945878149145990, - "venv/lib/python3.13/site-packages/h11/_writers.py": 8585823094975171480, - "venv/lib/python3.13/site-packages/pygments/lexers/apl.py": 16767474673619335683, - "venv/lib/python3.13/site-packages/fastapi/datastructures.py": 727178955746650271, - "docs/api/html/group__get__environments.html": 1082546386773301101, - "docs/api/html/group__build__summary.html": 14095423152800582584, - "venv/lib/python3.13/site-packages/jeepney/io/__init__.py": 2568357146184259175, - "docs/api/html/group__publish__candidate.html": 1676322270799999151, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_scalar_compat.py": 10068989979144965125, - "venv/lib/python3.13/site-packages/pyasn1/type/opentype.py": 340146737126677655, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli/_parser.py": 3540203683732744499, - "docs/api/html/group__test__load__tasks.html": 16906154469503701361, - "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/__init__.py": 15130871412783076140, - "docs/api/html/group__test__list__gitea__repositories__rejects__non__gitea.html": 8527146536000949913, - "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.pyi": 12275242654430570028, - "venv/lib/python3.13/site-packages/pygments/lexers/objective.py": 10966124906622044512, - "docs/api/html/group__test__backup__dashboards__pre__checks.html": 9786713373502698624, - "docs/api/html/group__ClarificationStateResult.html": 2917799947820497100, - "docs/api/html/doxygen__docs_8h.js": 3095005433054142131, - "venv/lib/python3.13/site-packages/jeepney/tests/test_low_level.py": 3579024079580951234, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/OSA_py.py": 2494967094400350388, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/_json.py": 6718137926109998531, - "venv/lib/python3.13/site-packages/pygments/lexers/felix.py": 2334148799642808111, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_asfreq.py": 6162659555626011770, - "venv/lib/python3.13/site-packages/numpy/typing/tests/test_runtime.py": 12534621594420844770, - "docs/api/html/group__SupersetLookupService.html": 18424905745600548459, - "venv/lib/python3.13/site-packages/attr/converters.pyi": 17675091788614033166, - "docs/api/html/group__handleCreateGiteaRepo__Function.html": 15118530305807816244, - "docs/api/html/group__DatasetReviewMappingModels.html": 18079997751810017590, - "venv/lib/python3.13/site-packages/pluggy/_manager.py": 18202550145040079612, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_interval.py": 1228451182688768814, - "frontend/playwright-report/trace/playwright-logo.svg": 4728037962520658573, - "venv/lib/python3.13/site-packages/numpy/ma/tests/test_core.py": 12712110303898046818, - "docs/api/html/group__ValidationApiTests.html": 11399974010057798721, - "docs/api/html/group____extract__created__id.html": 16195882368814063060, - "docs/api/html/group__get__check__status.html": 4939629494472656154, - "docs/api/html/group____resolve__task__name.html": 2876621474958502481, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/qt.py": 12828696264909048448, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_loop.py": 6502725909989655118, - "venv/lib/python3.13/site-packages/passlib/tests/test_totp.py": 9150389741890748343, - "docs/api/html/group____encode__sql__value.html": 14512027224697252876, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/selectable.py": 14646717349365560549, - "docs/api/html/group__TestStorageAuditFixes.html": 9774243051070118436, - "venv/lib/python3.13/site-packages/rsa/common.py": 10921682819204792805, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/conftest.py": 25473177671450048, - "docs/api/html/group__test__put__settings.html": 6582837714267844402, - "docs/api/html/group__parse__dashboard__url__for__filters__async.html": 8302124891449146063, - "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_period.py": 6255181868181182780, - "docs/api/html/group__TestBuildReportQueryString__Class.html": 12539253061507680489, - "venv/lib/python3.13/site-packages/pygments/lexers/savi.py": 95400050302239274, - "docs/api/html/group__ProfileUtils.html": 7099024507188160608, - "docs/api/html/tab_bd.png": 7964491274183929560, - "docs/api/html/group__SemanticSourceDto.html": 10276129549826437552, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.pyx": 17138729833735744777, - "venv/lib/python3.13/site-packages/secretstorage/defines.py": 9590959702075554048, - "venv/lib/python3.13/site-packages/numpy/__config__.py": 15794735729525094396, - "venv/lib/python3.13/site-packages/pandas/core/indexers/__init__.py": 1756397319068017890, - "docs/api/html/group__DashboardsApiTests.html": 1833223301110054529, - "venv/lib/python3.13/site-packages/websockets/legacy/auth.py": 16685128086396181286, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_np_datetime.py": 18363183516409621205, - "docs/api/html/group__test__global__settings__propagates__storage.html": 7088086585393016607, - "venv/lib/python3.13/site-packages/pandas/core/ops/array_ops.py": 10061020479033421596, - "docs/api/html/group__disable__schedule.html": 12452994962801576996, - "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/RECORD": 12733654377842895588, - "backend/src/services/validation_service.py": 4040391162969506763, - "docs/api/html/group__update__user.html": 16743247671966182710, - "docs/api/html/group__start.html": 18303777950604936330, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_iterrows.py": 3791886201566670087, - "venv/lib/python3.13/site-packages/numpy/ma/__init__.py": 214772578978182427, - "docs/api/html/group__test__datasets__api.html": 13468016019586118937, - "venv/lib/python3.13/site-packages/uvicorn/_subprocess.py": 13965754246493254447, - "backend/src/plugins/translate/__tests__/test_orthogonal_fixes.py": 12993935170413202399, - "frontend/src/lib/components/ui/DatabaseSearchCombobox.svelte": 13085059317395294999, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token.py": 4747545058753492487, - "docs/api/html/group__ModelsPackage.html": 13346504466563987278, - "docs/api/html/deprecated.html": 7802050220531259948, - "docs/api/html/search/groups_f.js": 14632289900069259892, - "docs/api/html/group__update__field__semantic.html": 3989002979285594854, - "venv/lib/python3.13/site-packages/apscheduler/executors/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_period.py": 3419297908709788013, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-exp.csv": 601782991752922510, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/protocol.py": 932296179250981622, - "backend/src/services/reports/normalizer.py": 13510015789619559475, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_reduction.py": 1049551473665255648, - "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.py": 9409852116555866847, - "docs/api/html/group__GitEnvironmentRoutes.html": 2128130501766405947, - "venv/lib/python3.13/site-packages/jeepney/tests/test_bindgen.py": 8637878018232418583, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/literal.py": 5641382379370828704, - "docs/api/html/group__apply__preview.html": 12571622568156907711, - "docs/api/html/group__ClarificationAnswerRequest.html": 11605609458195798499, - "venv/lib/python3.13/site-packages/pandas/_libs/reshape.pyi": 88721900806893992, - "backend/src/api/routes/translate/_schedule_routes.py": 18139325016758974063, - "docs/api/html/group__CleanReleaseTUI.html": 9540635457913551614, - "docs/api/html/group__SupersetClientFetchAllPages.html": 16697015167259947602, - "docs/api/html/group__ProfileModels.html": 9971230327622812343, - "docs/api/html/group__generate__upsert__sql.html": 1809776775004922247, - "venv/lib/python3.13/site-packages/anyio/abc/__init__.py": 11428476337116642278, - "venv/lib/python3.13/site-packages/pip/_internal/operations/prepare.py": 13514839972066861359, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_indexing_slow.py": 12558235638488427077, - "venv/lib/python3.13/site-packages/PIL/Hdf5StubImagePlugin.py": 15654153796225833950, - "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/__init__.py": 10834852742917144496, - "venv/lib/python3.13/site-packages/pygments/lexers/nix.py": 15050086330545600356, - "docs/api/html/group__TranslateDictionariesApi.html": 3796476465563433653, - "venv/lib/python3.13/site-packages/pygments/lexers/data.py": 9365024327689738663, - "venv/lib/python3.13/site-packages/pygments/__main__.py": 13200500591994479045, - "backend/src/services/clean_release/__tests__/test_compliance_orchestrator.py": 15537957042757255513, - "docs/api/html/group__security__badge__service.html": 15450560198932649899, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/cymysql.py": 5570881524647789741, - "docs/api/html/group__MigrationArchiveParser.html": 5158138547033150024, - "venv/lib/python3.13/site-packages/urllib3/py.typed": 12220177918227495093, - "venv/lib/python3.13/site-packages/smmap/test/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/content": 12465123883296161860, - "venv/lib/python3.13/site-packages/pandas/tests/libs/test_join.py": 5982692266872978444, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/cursor.py": 8074527784886981710, - "backend/src/plugins/translate/dictionary_filter.py": 18047895364995512274, - "docs/api/html/group__handleBulkCommit__Function.html": 13824296665546126425, - "docs/api/html/group__test__detect__virtual__columns.html": 7581525979923931052, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/RECORD": 173225030102092319, - "venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py": 11015954144633661960, - "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/RECORD": 16342326945648353387, - "frontend/build/_app/immutable/chunks/B2x1wvBz.js": 18181710553832181445, - "docs/api/html/group__TermCorrectionPopup.html": 7641550414720902162, - "docs/api/html/group__docker__backend__Dockerfile.html": 9397362251378611899, - "docs/api/html/group__handleGenerateMessage__Function.html": 11846603189188412357, - "venv/lib/python3.13/site-packages/fastapi/dependencies/__init__.py": 15130871412783076140, - "docs/api/html/group__test__parse__datetime__iso__string.html": 8814123590453372630, - "venv/lib/python3.13/site-packages/pip/_vendor/packaging/licenses/__init__.py": 2635182244050911910, - "docs/api/html/group__Logger.html": 11994362780557213627, - "docs/api/html/group__ApproveMappingRequest.html": 3410582796828079029, - "venv/lib/python3.13/site-packages/pydantic/parse.py": 13750341403702794392, - "venv/lib/python3.13/site-packages/pygments/lexers/_mapping.py": 12987629396101138833, - "venv/lib/python3.13/site-packages/greenlet/TExceptionState.cpp": 17617354576371846109, - "venv/lib/python3.13/site-packages/pygments/lexers/markup.py": 10311057264960923433, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/device_code.py": 6712610002227849281, - "docs/api/html/group____is__masked__or__invalid__api__key.html": 15965724501625265254, - "venv/lib/python3.13/site-packages/websockets/uri.py": 264376168984862773, - "docs/api/html/group__RetentionPolicy.html": 12974101386887046578, - "docs/api/html/group__map__columns.html": 11522895411125783141, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_indexing.py": 11572911419313573163, - "docs/api/html/group__test__single__line__text.html": 2397299118737677285, - "venv/lib/python3.13/site-packages/numpy/char/__init__.py": 4428218097234308768, - "venv/lib/python3.13/site-packages/requests/adapters.py": 17161109043243436200, - "venv/lib/python3.13/site-packages/gitdb/test/test_util.py": 541144638852210407, - "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.py": 6142062990691556076, - "venv/lib/python3.13/site-packages/yaml/parser.py": 18021832376874455801, - "docs/api/html/group__handleSaveUser__Function.html": 4104256279914165704, - "docs/api/html/group__SuggestRequest.html": 4340073923627780117, - "frontend/src/services/taskService.js": 11368052191249479533, - "venv/lib/python3.13/site-packages/pip/_internal/utils/urls.py": 8698341051522816167, - "venv/lib/python3.13/site-packages/pandas/io/parsers/__init__.py": 11752833703934347122, - "venv/lib/python3.13/site-packages/pip/_internal/cli/progress_bars.py": 7584699216537478969, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_direct.py": 12680113511641199070, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler_py.py": 16189204441438239231, - "venv/lib/python3.13/site-packages/numpy/random/_examples/numba/extending.py": 16515485226251040454, - "docs/api/html/group__normalize__density.html": 4520562612418312413, - "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____getWorkspaceStateLabel.html": 3907642846891320094, - "docs/api/html/group__test__run__detail__not__found.html": 5832122095347665056, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7591/__init__.py": 348469143777130970, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_reductions.py": 633644255158859565, - "backend/tests/core/migration/test_archive_parser.py": 8027202509785909637, - "docs/api/html/group__TranslationLanguage.html": 10040359770577893237, - "docs/api/html/group____get__preference__row.html": 10336167497316255170, - "docs/api/html/group__load__excel__mappings.html": 1764004230841108037, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_deprecations.py": 6015734648984591027, - "venv/lib/python3.13/site-packages/attr/_typing_compat.pyi": 2823472697859409877, - "docs/api/html/group__compute__build__token__metadata.html": 12931039213712686625, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7518/util.py": 5252158959761945519, - "docs/api/html/group____resolve__markdown__datasource.html": 15564490152308108829, - "venv/lib/python3.13/site-packages/pip/_vendor/distro/__main__.py": 2688850704829116818, - "venv/lib/python3.13/site-packages/numpy/f2py/auxfuncs.py": 2234931733687557433, - "venv/lib/python3.13/site-packages/starlette/responses.py": 14081178833432955042, - "docs/api/html/group__test__git__plugin__fallback__creates__new.html": 3612866020087373120, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/masked/test_arrow_compat.py": 12986187285988432338, - "venv/lib/python3.13/site-packages/_pytest/assertion/rewrite.py": 2425990712758915831, - "docs/api/html/group__MaintenanceTaskResult.html": 15638476531187491715, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_warnings.py": 13071210233876253749, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_get_dummies.py": 16112674981222844375, - "venv/lib/python3.13/site-packages/numpy/ma/__init__.pyi": 1855615156913010107, - "frontend/build/_app/immutable/nodes/10.BMcx7HQ6.js": 17292795487925211032, - "docs/api/html/group__test__dataset__review__scoped__command__routes__field__semantics__update.html": 453203095227910539, - "venv/lib/python3.13/site-packages/websockets/speedups.c": 11800200877763464246, - "venv/lib/python3.13/site-packages/PIL/ImageFilter.py": 10974179993355630141, - "docs/api/html/group____patch__dashboard__metadata.html": 4454475099365896302, - "docs/api/html/group__CleanReleaseAuditLog.html": 13803004817531190907, - "backend/src/core/__tests__/test_superset_profile_lookup.py": 12374214375171590188, - "frontend/src/components/git/BranchSelector.svelte": 3899942492629852811, - "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/timeseries.py": 17641722582848932332, - "venv/lib/python3.13/site-packages/pip/_vendor/idna/codec.py": 14814999235499530522, - "docs/api/html/group__test__create__empty__permissions.html": 2212767387589401498, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/_writer.py": 15053365792216907149, - "venv/lib/python3.13/site-packages/pygments/lexers/_sql_builtins.py": 5906655440517434734, - "docs/api/html/group____dataset__review__session.html": 17789442203180064219, - "venv/lib/python3.13/site-packages/pip/_internal/commands/uninstall.py": 11003472265011262515, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_api.py": 13739775047493489785, - "venv/lib/python3.13/site-packages/charset_normalizer/models.py": 9917300217816275821, - "frontend/src/lib/ui/Input.svelte": 1114920008107220678, - "backend/src/services/superset_lookup_service.py": 131886522963644068, - "docs/api/html/group__get__dataset__detail.html": 14957609761194222972, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iat.py": 3570464615146398178, - "venv/lib/python3.13/site-packages/pygments/lexers/phix.py": 16775285097768256238, - "venv/lib/python3.13/site-packages/greenlet/TThreadStateCreator.hpp": 9887867766190459665, - "venv/lib/python3.13/site-packages/pygments/formatters/img.py": 18230180171537565208, - "frontend/build/_app/immutable/chunks/BGXtRGj2.js": 17304982641954297474, - "docs/api/html/group__DashboardHub__handleSort__Function.html": 13034575267790584101, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_integrity.py": 12010564145080323987, - "docs/api/html/group__loadBranches__Function.html": 2222762227047931041, - "backend/src/api/routes/git/_repo_lifecycle_routes.py": 410621371876058173, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm32_ios.h": 6751259472945638998, - "backend/src/services/clean_release/policy_resolution_service.py": 1025435122296379201, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_strings.py": 17398806130402098912, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 2991745295485163924, - "docs/api/html/group__clean__release__exceptions.html": 16008981423566577546, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/url.py": 9692934100602135106, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/associationproxy.py": 7421910137064160936, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/groupby.py": 12749730954888633240, - "venv/lib/python3.13/site-packages/pandas/tests/util/__init__.py": 15130871412783076140, - "docs/api/html/group__trigger__sync__now.html": 3396264963221246266, - "docs/api/html/group__handleGenerateDocs__Function.html": 10904959743671367375, - "venv/lib/python3.13/site-packages/pygments/lexers/parasail.py": 7758704230750989282, - "venv/lib/python3.13/site-packages/passlib/tests/__main__.py": 16477180140297957650, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parsing.py": 2228959015420278458, - "venv/lib/python3.13/site-packages/PIL/SgiImagePlugin.py": 6916200065790425497, - "venv/lib/python3.13/site-packages/pandas/tests/interchange/test_spec_conformance.py": 16448324304809345527, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/chararray.pyi": 9683985931409244216, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_string_arrow.py": 10008395772220468318, - "venv/lib/python3.13/site-packages/pygments/lexers/_scilab_builtins.py": 17453812809700086888, - "backend/src/services/notifications/__init__.py": 16179868089470006686, - "venv/lib/python3.13/site-packages/pandas/tests/internals/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/__init__.py": 15130871412783076140, - "backend/src/api/routes/dashboards/_detail_routes.py": 3246373568851818130, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py": 14619529283550788275, - "docs/api/html/group__remove__future.html": 11104884545239660137, - "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/__init__.py": 8880435562116660648, - "venv/lib/python3.13/site-packages/pandas/plotting/_core.py": 6766059152026241747, - "venv/lib/python3.13/site-packages/ecdsa/errors.py": 11755424742691057760, - "backend/tests/services/clean_release/test_compliance_task_integration.py": 1554378264976811661, - "docs/api/html/group__frontend__src__services__git__utils__js____resolveDefaultConfig.html": 13108374365819353481, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_infer_dtype.py": 1647214604678171565, - "docs/api/html/group__test__get__clarification__state__returns__empty__payload__when__session__has__no__record.html": 13354220940558115259, - "venv/lib/python3.13/site-packages/pandas/tests/io/formats/__init__.py": 15130871412783076140, - "backend/tests/core/migration/test_dry_run_orchestrator.py": 5618205549212894117, - "docs/api/html/group__logout.html": 16684691150600527260, - "docs/api/html/group__dataset__review__api__dependencies.html": 4669339577033010960, - "venv/lib/python3.13/site-packages/pandas/core/arrays/floating.py": 1215586266958421991, - "backend/src/core/task_manager/task_logger.py": 6134165406387688582, - "venv/lib/python3.13/site-packages/pydantic/experimental/__init__.py": 492399139963056461, - "docs/api/html/group__DashboardListingRoutes.html": 4195282586959114740, - "venv/lib/python3.13/site-packages/passlib/tests/test_utils_md4.py": 15270721044910698449, - "docs/api/html/group__test__git__plugin__uses__shared__config__manager.html": 7129798876556226875, - "venv/lib/python3.13/site-packages/keyring-25.7.0.dist-info/RECORD": 10264342250058307301, - "docs/api/html/group__test__compliance__run__missing__manifest__marks__task__failed.html": 11141720046946032723, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_numeric.py": 9383693732009547935, - "docs/api/html/group____character__block__fallback.html": 12363981885413560924, - "docs/api/html/group__ClarificationQuestionDto.html": 4864141145333401395, - "venv/lib/python3.13/site-packages/urllib3/_version.py": 839747992523118887, - "backend/git_repos/remote/test-repo.git/objects/c5/b992984fe856bd81206304a881faa3087937cf": 2441623644664166468, - "backend/src/__init__.py": 11250920420388310655, - "docs/api/html/group__GitManagerUnfinishedMergeIntegrationTest__Module.html": 5158591590774869996, - "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__permalink__key.html": 12241603215085833674, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_memmap.py": 8709963808115492688, - "docs/api/html/group__BatchApproveMappingRequest.html": 2370830240828093817, - "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.py": 14446508217220482719, - "frontend/src/lib/components/dataset-review/SourceIntakePanel.svelte": 2392958189014264590, - "backend/src/api/auth.py": 14692769082165694028, - "docs/api/html/group__DashboardModels.html": 15051964313793389785, - "backend/test.db": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/resample/test_resampler_grouper.py": 8388490036390951341, - "backend/src/api/routes/dashboards/_action_routes.py": 10305887713397749487, - "venv/lib/python3.13/site-packages/anyio/abc/_eventloop.py": 2023190981288767991, - "venv/lib/python3.13/site-packages/starlette-0.50.0.dist-info/WHEEL": 2357997949040430835, - "docs/api/html/group__PreviewAcceptResponse.html": 2553928199035482431, - "docs/api/html/group__LoginPage.html": 5394223316995061464, - "docs/api/html/group__GitRepoLifecycleRoutes.html": 8015073860128542249, - "docs/api/html/group__test__dataset__dashboard__relations__script.html": 682612274542588295, - "docs/api/html/nav_fd.png": 7284442651853922987, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/_internal_utils.py": 4091305333167213279, - "frontend/build/_app/immutable/nodes/22.DEzvIFDT.js": 9396511863397825066, - "docs/api/html/group__DashboardHub__handleRestoreProfileFilter__Function.html": 13810703798808089221, - "docs/api/html/group__SupersetContextExtractorPII.html": 8059785191565221125, - "docs/api/html/group__GitServiceContractTests.html": 220168520751414086, - "docs/api/html/navtreeindex7.js": 3618174960812941697, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/numerictypes.pyi": 15565573472138344755, - "docs/api/html/group____ensure__conversation.html": 4799209468607073839, - "venv/lib/python3.13/site-packages/ecdsa/__init__.py": 6237633580090653788, - "backend/alembic/versions/b1c2d3e4f5a6_add_source_hash_to_translation_records.py": 4775534557742900399, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_pyf_src.py": 12351476647369971852, - "docs/api/html/group__TestTaskPersistenceHelpers.html": 8081661106396480485, - "docs/api/html/group__read__root.html": 3120255621105556309, - "venv/lib/python3.13/site-packages/pandas/_libs/index.cpython-313-x86_64-linux-gnu.so": 18409579812171549318, - "docs/api/html/group__TestGetReportsAsync__Class.html": 7630216116279114545, - "venv/lib/python3.13/site-packages/referencing/tests/test_referencing_suite.py": 8875120876162738440, - "docs/api/html/group__test__log__methods__delegation.html": 9316206776804683442, - "venv/lib/python3.13/site-packages/numpy/core/__init__.pyi": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/cache.py": 15037055977924167732, - "docs/api/html/group__ErrorContext.html": 3882007036049607733, - "docs/api/html/group__MappingResponse.html": 14098548275925135132, - "venv/lib/python3.13/site-packages/pandas/io/parsers/c_parser_wrapper.py": 5841064965672872661, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libselinux-0922c95c.so.1": 16516617710645376769, - "frontend/build/_app/immutable/chunks/UPZEHqRP.js": 5650828200456623395, - "venv/lib/python3.13/site-packages/pygments/lexers/prql.py": 1159462562014166646, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/RECORD": 11904769847385571600, - "venv/lib/python3.13/site-packages/_pytest/python_api.py": 12953851690889336092, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_droplevel.py": 2071197388394658690, - "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_reductions.py": 1713831062142840146, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_regression.py": 16293647824734937323, - "venv/lib/python3.13/site-packages/_pytest/threadexception.py": 10464996327276961035, - "docs/api/html/group__sync__dashboard.html": 8986119955907540350, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/path_registry.py": 8410829659513707067, - "docs/api/html/group__CompiledSQLPreview.html": 3548673488714120828, - "docs/api/html/group__test__tasks__logs__module.html": 17382599826426935479, - "venv/lib/python3.13/site-packages/pydantic/type_adapter.py": 10162988530225478692, - "docs/api/html/group__create__git__config.html": 3230144786855183113, - "backend/src/models/dataset_review_pkg/_execution_models.py": 13278357366267031549, - "docs/api/html/group__delete__user.html": 17189025779640639203, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/foo_deps.f90": 14350239197565389508, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_indexing.py": 1926721846569184112, - "docs/api/html/group__Utils__Module.html": 1214868907405242018, - "venv/lib/python3.13/site-packages/urllib3/response.py": 1987354357306756790, - "venv/lib/python3.13/site-packages/dateutil/utils.py": 7067026915090881998, - "venv/lib/python3.13/site-packages/pandas/_libs/byteswap.cpython-313-x86_64-linux-gnu.so": 17856848989827220388, - "venv/lib/python3.13/site-packages/pygments/lexers/lilypond.py": 1088755646568511197, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_formats.py": 10263085276270778330, - "venv/lib/python3.13/site-packages/pandas/tests/dtypes/test_dtypes.py": 13065857166166716628, - "docs/api/html/group__TASK__TYPE__PROFILES.html": 4360269911551960850, - "venv/lib/python3.13/site-packages/attr/_funcs.py": 5302917072419624193, - "venv/lib/python3.13/site-packages/numpy/random/mtrand.pyi": 17144590106028661128, - "docs/api/html/group__update__migration__settings.html": 16961141318650899544, - "docs/api/html/group____match__dataset__review__field.html": 2398745550798139744, - "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.pyi": 18395688193449688360, - "frontend/src/routes/admin/settings/llm/+page.svelte": 17653948269643098768, - "venv/lib/python3.13/site-packages/pandas/compat/pyarrow.py": 13693379998785127538, - "docs/api/html/group__MaintenanceEventListResponse.html": 11340834971136314876, - "frontend/src/lib/stores/__tests__/taskDrawer.test.js": 9115135514077743547, - "docs/api/html/group__test__save__config__syncs__environment__records__for__fk__backed__flows.html": 13497171163902546222, - "venv/lib/python3.13/site-packages/gitdb/utils/encoding.py": 1668793658272579497, - "venv/lib/python3.13/site-packages/pygments/lexers/math.py": 1647133236144311250, - "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_compression.py": 8577756801858495168, - "venv/lib/python3.13/site-packages/numpy/lib/_array_utils_impl.pyi": 11535882608355652318, - "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/parsing.pyi": 11967691665766452312, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tanh.csv": 6674536825156196036, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/ec.pyi": 9835891299056543360, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_operators.py": 12686786829906210592, - "docs/api/html/group__AutomationPage.html": 6103305575315188006, - "docs/api/html/group__delete__logs__for__tasks.html": 8497322347744097527, - "frontend/build/_app/immutable/chunks/C9YJaesY.js": 17698859127816820470, - "docs/api/html/group__test__headless__fallback.html": 12179006034022319490, - "docs/api/html/group__REASONING__OVERHEAD.html": 6648865310859009739, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/gevent.py": 4811840397266785551, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/constants.pyi": 4415188581374004936, - "docs/api/html/group____get__owned__session__or__404.html": 394393571117594920, - "docs/api/html/group__test__models.html": 5352406217241507006, - "venv/lib/python3.13/site-packages/attr/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkrb5-fcafa220.so.3.3": 5620628698712971642, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/WHEEL": 5160964596297665692, - "venv/lib/python3.13/site-packages/pip/_internal/cli/req_command.py": 2887890799766730546, - "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937_regressions.py": 8981142329261965538, - "venv/lib/python3.13/site-packages/httpcore/_backends/base.py": 5491727479625093183, - "docs/api/html/group__TaskGraph.html": 14527954946833016645, - "venv/lib/python3.13/site-packages/pandas/tests/frame/__init__.py": 15130871412783076140, - "backend/src/plugins/translate/__tests__/test_target_schema.py": 2950009660172053425, - "docs/api/html/group____execute__insert__sql.html": 14665396960311782890, - "venv/lib/python3.13/site-packages/pandas/core/util/hashing.py": 13106575357604888913, - "docs/api/html/group__orchestrator__config.html": 16901111520740994959, - "venv/lib/python3.13/site-packages/fastapi/middleware/wsgi.py": 3706578020491442104, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/__init__.py": 15130871412783076140, - "docs/api/html/group__ProfilePreferenceUpdateRequest.html": 4083205289293169459, - "docs/api/html/group__test__get__remote__id__returns__integer.html": 10764704660366053638, - "docs/api/html/group__run__manifest__build.html": 9207320813511287781, - "docs/api/html/group__test__get__repository__status__batch__deduplicates__and__truncates__ids.html": 17068511664214583355, - "docs/api/html/group__DashboardURLFilterExtraction.html": 4077034173336465746, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/asymmetric_key.py": 14948661854506285135, - "docs/api/html/group__DictionaryEntryResponse.html": 14840442294467132189, - "venv/lib/python3.13/site-packages/pip/_internal/distributions/sdist.py": 9490366272695301385, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_polynomial.pyi": 4266795948450921502, - "venv/lib/python3.13/site-packages/pandas/io/sas/__init__.py": 8095743504390517992, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/__init__.py": 15487893533770969571, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/__init__.py": 12290228927283601067, - "venv/lib/python3.13/site-packages/httpcore/_sync/http2.py": 11167798787131917524, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.f": 1486878017876900890, - "frontend/build/_app/immutable/nodes/39.Dv1h6Fxq.js": 11914548040937603474, - "backend/src/models/profile.py": 12524181549230399253, - "venv/lib/python3.13/site-packages/pygments/lexers/gdscript.py": 12775803139854052838, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_base.py": 16710163879060522196, - "docs/api/html/group__bundle__build__mode.html": 16344595277983678406, - "venv/lib/python3.13/site-packages/uvicorn/lifespan/__init__.py": 15130871412783076140, - "backend/git_repos/remote/test-repo.git/hooks/fsmonitor-watchman.sample": 14381968406977928721, - "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_openid.py": 5670082466989878756, - "venv/lib/python3.13/site-packages/pandas/core/interchange/dataframe_protocol.py": 12857147443873820285, - "venv/lib/python3.13/site-packages/pip/_internal/models/link.py": 7172346414535657925, - "docs/api/html/search/functions_15.js": 3151355335604935323, - "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_indexing.py": 14461606056665485421, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/period/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.abi3.so": 8691731321358529264, - "venv/lib/python3.13/site-packages/PIL/ImageText.py": 15322496455608242001, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/processors.py": 4817637175145012571, - "venv/lib/python3.13/site-packages/sqlalchemy/event/legacy.py": 13465911776953636949, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/base.py": 14734745827490220667, - "backend/src/api/routes/assistant/_dispatch.py": 9318875739506706096, - "venv/lib/python3.13/site-packages/jsonschema_specifications/tests/test_jsonschema_specifications.py": 12693210357718751894, - "venv/lib/python3.13/site-packages/pip/_internal/operations/__init__.py": 15130871412783076140, - "docs/api/html/group__checkout__branch.html": 2135218692165280454, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/gh19161.f90": 7691871466158049079, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_nlargest.py": 1423106021399469694, - "venv/lib/python3.13/site-packages/pip/_internal/commands/debug.py": 13528633965821062950, - "docs/api/html/group__CleanReleaseV2ApiTests.html": 17398513879675376238, - "docs/api/html/group____parsing__imports.html": 14931596898828785688, - "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/utils.h": 3920393832993063886, - "docs/api/html/search/mag_seld.svg": 5172700322779250184, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/scripts.py": 8759605303886441976, - "docs/api/html/group____resolve__profile__actor__aliases.html": 11823740193965915284, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_indexing.py": 8943385096811168079, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/traversals.py": 1188980668212022868, - "frontend/src/routes/datasets/review/__tests__/dataset_review_entry.test.js": 18445680453131807754, - "venv/lib/python3.13/site-packages/httpx/_content.py": 1301854837767395687, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cos.csv": 12896264205153363031, - "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_astype.py": 4023134352880775277, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_mixed.py": 3476101699679108025, - "docs/api/html/group__TestArchiveParser.html": 15611159576834520636, - "docs/api/html/group__ValidationFinding.html": 10426135687421865521, - "docs/api/html/group__test__start__check__and__get__status__contract.html": 11086148140670150623, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/validator.py": 14200019408839159476, - "venv/lib/python3.13/site-packages/numpy/lib/_type_check_impl.pyi": 2667658052734929089, - "venv/lib/python3.13/site-packages/numpy/core/function_base.py": 12696640832638144604, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_dataframe.py": 14945138617962156057, - "venv/lib/python3.13/site-packages/bcrypt/__about__.py": 11196369962175692786, - "docs/api/html/group____generate__unique__name.html": 17753267115013703674, - "venv/lib/python3.13/site-packages/pandas/io/formats/style_render.py": 13586834368514801953, - "venv/lib/python3.13/site-packages/PIL/SpiderImagePlugin.py": 12234762087257163606, - "venv/lib/python3.13/site-packages/cffi/_shimmed_dist_utils.py": 4824806157907607487, - "docs/api/html/group__DashboardTaskHistoryItem.html": 1713647374003736687, - "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pycparser/lextab.py": 16045770806700381661, - "frontend/src/app.css": 3020720639702954139, - "venv/lib/python3.13/site-packages/git/refs/symbolic.py": 14996822770181935589, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py": 1041624619197012753, - "docs/api/html/group__openCreateModal__Function.html": 8404560973305150574, - "docs/api/html/group__SupersetClientGetDatasetLinkedDashboardCount.html": 4679132333842497993, - "venv/lib/python3.13/site-packages/pytest_asyncio-1.3.0.dist-info/METADATA": 12747626683405487434, - "venv/lib/python3.13/site-packages/numpy/polynomial/_polybase.pyi": 15864712173815045309, - "frontend/src/lib/components/dataset-review/__tests__/source_intake_panel.ux.test.js": 17228280298558782730, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/constrain.py": 6513876973118703677, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/__init__.py": 15130871412783076140, - "docs/api/html/group__auto__size__batches.html": 1896970568485171466, - "docs/api/html/group__test__ux__state__contracts.html": 17383839779640281908, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_multi_thread.py": 5660352470978621226, - "venv/lib/python3.13/site-packages/apscheduler/util.py": 11440701924954413440, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/rsa.py": 7852235015155694076, - "venv/lib/python3.13/site-packages/charset_normalizer/api.py": 9172704759115759638, - "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/py.typed": 9796674040111366709, - "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.cpython-313-x86_64-linux-gnu.so": 17652852308919396403, - "venv/lib/python3.13/site-packages/pip/_vendor/msgpack/__init__.py": 5896924546560667519, - "docs/api/html/group__get__next__executions.html": 11223768021455297815, - "docs/api/html/group____get__linked__dashboards.html": 4945121824645468559, - "docs/api/html/group__clear__tasks.html": 2396216755945302765, - "venv/lib/python3.13/site-packages/rapidfuzz/process_cpp.py": 16797760903996739079, - "backend/src/services/health_service.py": 7707107506464919176, - "venv/lib/python3.13/site-packages/pip/_vendor/__init__.py": 11734104142988812647, - "docs/api/html/group____normalize__lookup__payload.html": 11273830722028075585, - "docs/api/html/group__test__update__task__not__found.html": 14144153955621354369, - "docs/api/html/group__frontend__src__lib__stores__translationRun__js____startTranslationRun.html": 11102301560013944310, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_partial_slicing.py": 16219611534574666182, - "venv/lib/python3.13/site-packages/sqlalchemy/engine/__init__.py": 6327819603201859020, - "docs/api/html/group__AuthStore.html": 6433208268652425169, - "docs/api/html/group__SeedSupersetLoadTestScript.html": 2748544528766669921, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_subclass.py": 12453252499544981915, - "venv/lib/python3.13/site-packages/apscheduler/schedulers/twisted.py": 16059862619165885730, - "venv/lib/python3.13/site-packages/pygments/lexers/_cocoa_builtins.py": 15468347167912293394, - "venv/lib/python3.13/site-packages/ecdsa/test_pyecdsa.py": 14542286382742044648, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_subclass.py": 16342917051908786497, - "venv/lib/python3.13/site-packages/numpy/testing/__init__.pyi": 8126257788928282891, - "venv/lib/python3.13/site-packages/pandas/_libs/missing.pyi": 13543139288840944411, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/METADATA": 3044554568188160018, - "venv/lib/python3.13/site-packages/numpy/ctypeslib/__init__.pyi": 13208953770241664384, - "docs/api/html/group____resolve__dashboard__id__by__ref.html": 16216818858438055057, - "venv/lib/python3.13/site-packages/pyasn1/type/base.py": 6801504886392649787, - "backend/src/plugins/translate/dictionary.py": 4150581558765697237, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_alter_axes.py": 11338712836362534969, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_setops.py": 5208912445718807372, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/dml.py": 6567627855667533645, - "venv/lib/python3.13/site-packages/cryptography/hazmat/backends/openssl/__init__.py": 4870269250323682576, - "docs/api/html/group__SupersetDashboardsCrudMixin.html": 9711358306599693626, - "docs/api/html/group__test__get__remote__ids__batch__returns__dict.html": 9840452755032662260, - "venv/lib/python3.13/site-packages/pandas/tests/frame/test_ufunc.py": 5025605639221470838, - "venv/lib/python3.13/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE": 16499431172938528491, - "docs/api/html/group__test__empty__model__returns__false.html": 15426733626551187888, - "docs/api/html/group__FeaturesSettings.html": 5116404687015486795, - "docs/api/html/group__SessionCollaboratorDto.html": 13064735570529712848, - "venv/lib/python3.13/site-packages/pandas/core/util/numba_.py": 3535857372998653863, - "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/REQUESTED": 15130871412783076140, - "docs/api/html/group__fetchRunDetail__Function.html": 6020648800343509598, - "docs/api/html/group__test__list__jobs.html": 3648457057716850116, - "venv/lib/python3.13/site-packages/idna-3.11.dist-info/WHEEL": 8600534672961461758, - "docs/api/html/group__override__detected__language.html": 18199671765025303070, - "docs/api/html/group__AUTH__DATABASE__URL.html": 7086189906630592132, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/cmac.pyi": 16042272072158438261, - "docs/api/html/group____build__task__observability__summary.html": 8840583376339929930, - "docs/api/html/group__CleanPolicySnapshot.html": 4218818132714115677, - "docs/api/html/group__test__get__dashboards__env__not__found.html": 430793696501404989, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_export_format.py": 17862222951776199694, - "venv/lib/python3.13/site-packages/numpy/f2py/__init__.pyi": 3566887042414273892, - "docs/api/html/group__MaintenanceStore.html": 3766820613250188744, - "backend/tests/test_core_scheduler.py": 2305397851769292578, - "backend/src/core/__init__.py": 8673978627093182792, - "backend/tests/test_translate_corrections.py": 6483644195889451514, - "docs/api/html/group__TestDemoModeIsolation.html": 16048106037006305955, - "venv/lib/python3.13/site-packages/pygments/lexers/oberon.py": 15220143799603451830, - "backend/src/plugins/translate/prompt_builder.py": 11939931262286362350, - "venv/lib/python3.13/site-packages/pandas/core/series.py": 11618272682977710696, - "venv/lib/python3.13/site-packages/websockets/asyncio/messages.py": 6848925149930551457, - "docs/api/html/group__test__git__service__repositorys__typo__regression.html": 12032073559759337916, - "docs/api/html/group____FakeQuery.html": 2190196221479025836, - "docs/api/html/search/pages_1.js": 3711959322811078222, - "backend/src/core/superset_client/_datasets_preview.py": 15457586484984946621, - "backend/tests/services/clean_release/test_candidate_manifest_services.py": 4252479336748536986, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/revocation.py": 5718403706971163960, - "docs/api/html/group__onDestroy__Function.html": 1351684727316654046, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_astype.py": 15384141282611151560, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hashes.pyi": 7385072545198817771, - "venv/lib/python3.13/site-packages/numpy/lib/_arraysetops_impl.py": 5583478355093966990, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/common/test_inf.py": 8659210689723088321, - "docs/api/html/plus.svg": 9293766439275217251, - "venv/lib/python3.13/site-packages/pygments/lexers/scdoc.py": 8481749542463754279, - "frontend/src/components/tools/MapperTool.svelte": 3019722694056442354, - "venv/lib/python3.13/site-packages/pandas/tests/test_errors.py": 10116716310156374885, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_quarter.py": 11099677280196239755, - "docs/api/html/group__locale__Store.html": 3681806005291514520, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/assumed_shape/foo_mod.f90": 5765181406654284736, - "docs/api/html/group__DictionariesPage.html": 10830021443332069379, - "docs/api/html/group__GitLLMExtensionModule.html": 5836066463515389762, - "venv/lib/python3.13/site-packages/pygments/lexers/yang.py": 7409365668425173823, - "docs/api/html/group__SidebarTest.html": 12608676498908713689, - "venv/lib/python3.13/site-packages/pygments/lexers/idl.py": 11454152060351801696, - "docs/api/html/group__DashboardHub__getColumnCellValue__Function.html": 10318545865816410175, - "docs/api/html/group__MaintenanceChartManager.html": 457454489502690100, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/ciphers/modes.py": 6832635056228445650, - "backend/src/models/mapping.py": 17370626239851802183, - "backend/src/plugins/translate/__tests__/test_dictionary_correction.py": 17508858180216288123, - "venv/lib/python3.13/site-packages/numpy/__config__.pyi": 1422114139050700402, - "docs/api/html/group____has__any__permission.html": 3803466462586983731, - "docs/api/html/group__update__validation__policy.html": 13352530740640222844, - "docs/api/html/group____matches__actor__case__insensitive.html": 218018109702683196, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_arithmetic.py": 13320788210970132239, - "venv/lib/python3.13/site-packages/httpx/__init__.py": 6383055693166097719, - "venv/lib/python3.13/site-packages/jose/backends/native.py": 50023158618000658, - "venv/lib/python3.13/site-packages/pandas/util/_validators.py": 10201915528715968165, - "venv/lib/python3.13/site-packages/pandas/_config/localization.py": 11197430792343731762, - "docs/api/html/group__test__register__candidate__rejects__empty__artifact__set.html": 16268783415250759980, - "frontend/src/routes/datasets/DatasetList.svelte": 12931773180198281284, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____closeDrawer.html": 8754263409785426495, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_print.py": 598821365691498702, - "venv/lib/python3.13/site-packages/pandas/arrays/__init__.py": 10005095663840461954, - "venv/lib/python3.13/site-packages/pydantic/deprecated/parse.py": 819235411736647763, - "docs/api/html/group__delete__gitea__repository.html": 15330998186210577395, - "docs/api/html/group__DatasetReviewUs3UxTests__Module.html": 4798151615875850408, - "venv/lib/python3.13/site-packages/more_itertools-10.8.0.dist-info/WHEEL": 8600534672961461758, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/token.py": 11655733747403312358, - "docs/api/html/group__TranslateSchedulesApi.html": 2216535141847218372, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hashes.py": 8545380307201732345, - "docs/api/html/search/all_10.js": 7221993751774974193, - "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/__init__.pyi": 10784666243744187693, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_stringdtype.py": 8381476847196129074, - "venv/lib/python3.13/site-packages/PIL/IptcImagePlugin.py": 14329453455296600824, - "venv/lib/python3.13/site-packages/PIL/ImageFont.py": 15621336334706847865, - "venv/lib/python3.13/site-packages/git/objects/submodule/root.py": 5322248117564025370, - "backend/src/services/clean_release/artifact_catalog_loader.py": 16917420865314497219, - "venv/lib/python3.13/site-packages/passlib/handlers/django.py": 66638338316838351, - "venv/lib/python3.13/site-packages/pandas/io/_util.py": 13608922148814398500, - "venv/lib/python3.13/site-packages/pandas/core/array_algos/masked_accumulations.py": 11428556645065711062, - "docs/api/html/group__unsubscribe__logs.html": 14174454111096175560, - "venv/lib/python3.13/site-packages/pip/_internal/commands/search.py": 8292218231992728142, - "docs/api/html/group____collect__user__permission__pairs.html": 10134810142124377145, - "backend/src/services/dataset_review/orchestrator.py": 15350066045014880242, - "venv/lib/python3.13/site-packages/more_itertools/more.pyi": 4844596605492825806, - "venv/lib/python3.13/site-packages/numpy/core/overrides.py": 11228841530565061867, - "venv/lib/python3.13/site-packages/pygments/styles/friendly_grayscale.py": 16840106759769837649, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_common.py": 12064185052519470358, - "venv/lib/python3.13/site-packages/authlib/oidc/discovery/__init__.py": 308178910015974550, - "docs/api/html/group__StartSessionRequest.html": 4207823078857319758, - "docs/api/html/group__SidebarStoreTest__Module.html": 10062386876351645240, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_counting.py": 5628494712451120258, - "backend/src/scripts/delete_running_tasks.py": 4209804708500252012, - "docs/api/html/group__Navbar.html": 3616287054974977311, - "docs/api/html/group__test__get__datasets__pagination__zero__page.html": 13580035186306666421, - "docs/api/html/group__RepoStatusBatchResponse.html": 16247460233048160196, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/serialization/pkcs7.py": 9623344347519683527, - "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/INSTALLER": 17282701611721059870, - "venv/lib/python3.13/site-packages/pydantic/networks.py": 15767095796055034509, - "venv/lib/python3.13/site-packages/numpy/lib/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/__init__.py": 9884041726796476720, - "venv/lib/python3.13/site-packages/pandas/core/base.py": 10262717073058743378, - "docs/api/html/group__SupersetClientExtractOwnerLabels.html": 12596315108945733263, - "venv/lib/python3.13/site-packages/pygments/lexers/pawn.py": 18127303119414402594, - "docs/api/html/group__DashboardsRouter.html": 16823726392565419686, - "venv/lib/python3.13/site-packages/numpy/_core/_internal.py": 13056946834347643109, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/pyodbc.py": 11271266527234292020, - "docs/api/html/group__test__list__empty.html": 7117128937256101280, - "docs/api/html/group__build__candidate__manifest__v2__endpoint.html": 1880625190919477701, - "venv/lib/python3.13/site-packages/apscheduler/executors/asyncio.py": 2230610508870117058, - "venv/lib/python3.13/site-packages/pluggy/_hooks.py": 6941854999025808196, - "docs/api/html/group__fetch__paginated__count.html": 8726813622685250751, - "venv/lib/python3.13/site-packages/pydantic_settings/version.py": 17263591404988984480, - "docs/api/html/group__test__ensure__encryption__key__reuses__existing__env__file__value.html": 11987119925878454851, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/recfunctions.py": 18330899924171767482, - "venv/lib/python3.13/site-packages/pandas/api/interchange/__init__.py": 181911269762882069, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_repeat.py": 16971624301893342868, - "docs/api/html/group__AuthJwtModule.html": 11179759053901968829, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_constructors.py": 6515602441449955627, - "venv/lib/python3.13/site-packages/fastapi/_compat/shared.py": 5249003014594464718, - "venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py": 15060063324909302240, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/resolvers/abstract.py": 14216912708889351810, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_other.py": 783701524491896337, - "docs/api/html/group__test__register__candidate__rejects__malformed__artifact__input.html": 7952314594070523566, - "venv/lib/python3.13/site-packages/git/objects/fun.py": 15014209679156260865, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_construction.py": 13953424340346625390, - "venv/lib/python3.13/site-packages/numpy/matrixlib/defmatrix.pyi": 17903213294220335051, - "docs/api/html/group__CleanReleaseV2ReleaseApiTests.html": 394913399748993155, - "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np121.pkl.gz": 4708588000144933291, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____getTaskDrawerAutoOpenPreference.html": 5828119756126492160, - "docs/api/html/group____serialize__execution__mapping.html": 14633172635644173618, - "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_stat_reductions.py": 11569525436540769152, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/__init__.py": 15130871412783076140, - "frontend/src/routes/datasets/__tests__/bulk_actions.test.js": 8445672988319354987, - "venv/lib/python3.13/site-packages/click/core.py": 16793133181718891193, - "venv/lib/python3.13/site-packages/numpy/f2py/rules.py": 9322914583460630877, - "docs/api/html/group____parse__csv__enum__list.html": 13696325195084668398, - "docs/api/html/group__check__target__schema.html": 16422257862833670253, - "docs/api/html/sync_off.png": 12901520360373226022, - "docs/api/html/group____get__owned__field__or__404.html": 7076759849479749348, - "docs/api/html/group__test__map__columns__success.html": 9453179995162588276, - "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log2.csv": 13979868713429167140, - "venv/lib/python3.13/site-packages/numpy/_core/memmap.pyi": 14124415711399771715, - "docs/api/html/group__buildReportQueryString__Function.html": 10840805796711813208, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/claims.py": 12645788445743898136, - "venv/lib/python3.13/site-packages/numpy/fft/tests/test_helper.py": 5362180645056413158, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_indexing.py": 546024765756472196, - "docs/api/html/group____ensure__filter__source__enum__values.html": 15520428780478561060, - "venv/lib/python3.13/site-packages/_pytest/capture.py": 17850967053989495422, - "venv/lib/python3.13/site-packages/numpy/_utils/_convertions.pyi": 362674842164060822, - "venv/lib/python3.13/site-packages/uvicorn/protocols/__init__.py": 15130871412783076140, - "backend/alembic/versions/ed310b33f02c_multi_language_translation_tables.py": 1424919631205944664, - "docs/api/html/group__TestApprovalService.html": 4110308134003984759, - "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/licenses/LICENSE": 5669644120528099197, - "venv/lib/python3.13/site-packages/attrs/__init__.py": 9502968706258875910, - "venv/lib/python3.13/site-packages/httpx/_config.py": 8800538050293498928, - "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/signals.py": 13677199634497673845, - "venv/lib/python3.13/site-packages/ecdsa/test_jacobi.py": 1249543655136186433, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_aggregate.py": 15038915958422670700, - "docs/api/html/group__TaskPersistenceModule.html": 669406050270516144, - "docs/api/html/group__GitMergeRoutes.html": 7022556547485727423, - "venv/lib/python3.13/site-packages/pydantic/config.py": 1450073446492183677, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_shares_memory.py": 2911029123657970431, - "docs/api/html/group__test__string__literal__false__positive.html": 7083542074508752106, - "venv/lib/python3.13/site-packages/pandas/tests/window/test_dtypes.py": 2313400030798822026, - "frontend/src/services/adminService.js": 5974657342670197690, - "docs/api/html/group____build__values__clause.html": 14938421019054450990, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/__init__.py": 11283047029180207588, - "docs/api/html/group__PreviewRequest.html": 269103665065334891, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/screen.py": 6933591906297023845, - "venv/lib/python3.13/site-packages/_pytest/stepwise.py": 3527630676691417317, - "venv/lib/python3.13/site-packages/pydantic/json.py": 4109352930301629308, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_timedeltas.py": 1175883320586860493, - "venv/lib/python3.13/site-packages/apscheduler/jobstores/mongodb.py": 5310116401542377882, - "backend/src/schemas/profile.py": 3926676368643549433, - "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/METADATA": 14158024963562115116, - "backend/src/api/routes/__tests__/conftest.py": 3768471623650776808, - "backend/src/services/git/_sync.py": 18140332203522670777, - "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_set_value.py": 17480833128128893268, - "venv/lib/python3.13/site-packages/pygments/lexers/lisp.py": 11872331622183590758, - "docs/api/html/group__buildDatasetReviewRequestOptions__Function.html": 3581409884229047567, - "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.pyi": 9291444594181042963, - "docs/api/html/group__getStatusLabel__Function.html": 4868893043616825247, - "venv/lib/python3.13/site-packages/pandas/core/_numba/kernels/__init__.py": 10920760520224795596, - "venv/lib/python3.13/site-packages/psycopg2/__init__.py": 15798412693009721199, - "docs/api/html/group__test__get__reports__default__pagination__contract.html": 2422991810127559996, - "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_skiprows.py": 8764515842253490735, - "venv/lib/python3.13/site-packages/PIL/_imagingmath.cpython-313-x86_64-linux-gnu.so": 77725273398858074, - "venv/lib/python3.13/site-packages/pip/_internal/locations/__init__.py": 8147012724727395488, - "venv/lib/python3.13/site-packages/pygments/lexers/freefem.py": 9851243865301700231, - "venv/lib/python3.13/site-packages/pygments/formatters/other.py": 13124903290103898118, - "docs/api/html/group__export__documentation.html": 6283874611178772428, - "venv/lib/python3.13/site-packages/numpy/_core/numerictypes.pyi": 12144531601504755948, - "docs/api/html/group__report__repository.html": 57313121527708932, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_constructors.py": 979131987837682435, - "docs/api/html/group__get__candidate__overview__v2__endpoint.html": 3653749146753267302, - "docs/api/html/group__ExportArtifactResponse.html": 9908246940869758823, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/twodim_base.pyi": 10156504423258357540, - "venv/lib/python3.13/site-packages/pygments/lexers/email.py": 4355246279485460399, - "venv/lib/python3.13/site-packages/pydantic/aliases.py": 14481980467204711659, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/tests/strings/test_find_replace.py": 262184394614748386, - "backend/src/services/security_badge_service.py": 18061005101062867126, - "venv/lib/python3.13/site-packages/pygments/lexers/qlik.py": 3881663935630350152, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/base.py": 7855217087916771270, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_legend.py": 13094583566764796297, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/jwt_bearer.py": 6973859422068490274, - "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/accessor.py": 8373575175447947401, - "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/WHEEL": 5179340427739743287, - "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connection.py": 8190661510572400096, - "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/_cryptography_key.py": 388489172928969873, - "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/utils.py": 617314300548081590, - "docs/api/html/group__TranslationEvent.html": 10898166554501315310, - "venv/lib/python3.13/site-packages/numpy/ma/core.pyi": 4829891173366130328, - "docs/api/html/group__SupersetContextExtractorBase____extract__dashboard__id__from__state.html": 2544335115240783642, - "docs/api/html/group__reports.html": 6554651341588172263, - "venv/lib/python3.13/site-packages/pandas/core/groupby/groupby.py": 5536922766239875007, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexers.py": 6773751569163287047, - "docs/api/html/search/all_13.js": 14323409538596817569, - "venv/lib/python3.13/site-packages/pluggy/_version.py": 7195758686717994475, - "docs/api/html/group__test__future__end__time__skipped.html": 4190142045276084379, - "venv/lib/python3.13/site-packages/numpy/tests/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pandas/core/computation/scope.py": 16859504937881548797, - "frontend/build/_app/immutable/chunks/DgoIDw4h.js": 10605176285482573925, - "docs/api/html/group__RunExecutionService.html": 14883816129062720567, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/smmap-5.0.2.dist-info/zip-safe": 15240312484046875203, - "docs/api/html/group__getLoggingConfig__Function.html": 3509641423397753733, - "docs/api/html/group__GitServerConfig.html": 770018572005176165, - "docs/api/html/group__get__history.html": 18146329279871398582, - "venv/lib/python3.13/site-packages/jose/exceptions.py": 8562012933958941039, - "venv/lib/python3.13/site-packages/greenlet/greenlet.h": 16216120538647882082, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/declarative/extensions.py": 217122171232375592, - "backend/src/api/routes/translate/_helpers.py": 6946167155296110158, - "venv/lib/python3.13/site-packages/anyio/from_thread.py": 7973678341601453474, - "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_contains.py": 17630310725250553247, - "backend/src/models/dataset_review_pkg/_filter_models.py": 11282786315414885098, - "docs/api/html/group__ArtifactCatalogLoader.html": 15778534404992178161, - "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iloc.py": 7586049606891014270, - "venv/lib/python3.13/site-packages/pip/_internal/cli/main.py": 9304034369762028739, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/exclusions.py": 15852672500377116297, - "docs/api/html/group__test__allowed__origins__parsing.html": 12390096577494505113, - "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/padding.py": 11244071766694094554, - "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/json.py": 15085143436056608404, - "docs/api/html/group__get__sqllab__mappings.html": 10230808286410535014, - "venv/lib/python3.13/site-packages/pygments/lexers/pddl.py": 9256112535059995841, - "venv/lib/python3.13/site-packages/dotenv/py.typed": 9796674040111366709, - "docs/api/html/group____make__task.html": 17252776482294162924, - "docs/api/html/group__ApiKeyCreateResponse.html": 9522527415314064226, - "docs/api/html/group__test__candidate__lifecycle__transitions.html": 12801528720010351292, - "docs/api/html/group__ReportDetailView.html": 2581277258940002250, - "docs/api/html/group__test__configure__logger__task__log__level.html": 523141612683121323, - "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.py": 13012775703413938576, - "venv/lib/python3.13/site-packages/pandas/tests/plotting/__init__.py": 15130871412783076140, - "venv/lib/python3.13/site-packages/pygments/lexers/_asy_builtins.py": 1665617945818958781, - "docs/api/html/group__MaintenanceSchemasModule.html": 9161032253446542473, - "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/__init__.py": 5027355874704300832, - "venv/lib/python3.13/site-packages/jaraco_functools-4.4.0.dist-info/WHEEL": 16097436423493754389, - "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath.py": 13010939919217862164, - "docs/api/html/group__DatasetReviewSchemas.html": 5120136138050590201, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/RECORD": 18314788057673509075, - "docs/api/html/group__retry__run.html": 9488774948219266966, - "venv/lib/python3.13/site-packages/charset_normalizer/md__mypyc.cpython-313-x86_64-linux-gnu.so": 12629680116730190225, - "docs/api/html/group__test__list__banners.html": 725331980400835161, - "venv/lib/python3.13/site-packages/numpy/lib/user_array.py": 14003456655595459585, - "venv/lib/python3.13/site-packages/fastapi/cli.py": 13972351579061154035, - "venv/lib/python3.13/site-packages/gitdb/db/git.py": 13552203494737962026, - "venv/lib/python3.13/site-packages/cryptography/x509/name.py": 15338066443257445241, - "venv/lib/python3.13/site-packages/numpy/f2py/setup.cfg": 17974180080760231128, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_take.py": 3281943380342853080, - "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_append_common.py": 2377351120083195530, - "venv/lib/python3.13/site-packages/passlib/context.py": 10362592407593745107, - "venv/lib/python3.13/site-packages/pandas/tests/series/test_validate.py": 2133732492454460539, - "docs/api/html/group__test__no__header__returns__none.html": 15501842573100321816, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_between_time.py": 14266333058913745751, - "venv/lib/python3.13/site-packages/pip/_vendor/distlib/metadata.py": 1362166613223529108, - "docs/api/html/group__DashboardTypes.html": 9536763043691441999, - "venv/lib/python3.13/site-packages/sqlalchemy/sql/lambdas.py": 13377298868598623228, - "venv/lib/python3.13/site-packages/pandas/tests/apply/test_frame_apply.py": 8103417303073393969, - "frontend/build/_app/immutable/chunks/Ds3VwoRu.js": 15219158071418187569, - "venv/lib/python3.13/site-packages/jaraco/classes/py.typed": 15130871412783076140, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/spinner.py": 5564789578696016201, - "venv/lib/python3.13/site-packages/pandas/tests/test_aggregation.py": 16863055892192070549, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/METADATA": 15225661995655327539, - "docs/api/html/group__AssistantCommandParser.html": 5740092626323461301, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_reductions.py": 17810094294132819208, - "docs/api/html/globals_func_q.html": 11705790463608300649, - "docs/api/html/group__test__json__load__if__needed__dict.html": 1232447365145854146, - "docs/api/html/group__test__detect__language__basic.html": 13601975669344229958, - "venv/lib/python3.13/site-packages/greenlet/tests/test_greenlet_trash.py": 6191655521667576604, - "venv/lib/python3.13/site-packages/pandas/core/apply.py": 17080038932032802984, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/dtype.py": 5553691086633215767, - "docs/api/html/group__MaintenanceDashboardBannerState.html": 9967895785618239725, - "venv/lib/python3.13/site-packages/authlib/oidc/core/errors.py": 18154419146257747681, - "venv/lib/python3.13/site-packages/pandas/core/shared_docs.py": 2173578869755325502, - "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/hybrid.py": 14287665090144592501, - "docs/api/html/group__TrustLevel.html": 10252007574843682167, - "venv/lib/python3.13/site-packages/pydantic/_internal/_serializers.py": 12077180322353709930, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_quantile.py": 11273403743157110261, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/_pick.py": 15760492005471315814, - "venv/lib/python3.13/site-packages/passlib/tests/test_crypto_digest.py": 7988469818593853943, - "venv/lib/python3.13/site-packages/pycparser/ply/ygen.py": 5520340773074632724, - "backend/src/services/clean_release/repositories/manifest_repository.py": 12161285893423159929, - "docs/api/html/group__AssistantLlmPlannerIntent.html": 17231865817068748918, - "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_fillna.py": 4610666845291315333, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/filesize.py": 13234856188049714485, - "docs/api/html/group__test__get__job__not__found.html": 486301214235859142, "docs/api/html/group__TestReportsApi.html": 6588129911944894739, - "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/INSTALLER": 17282701611721059870, - "frontend/build/_app/immutable/nodes/40.BLbAdNlB.js": 14899574486455796001, - "docs/api/html/search/functions_4.js": 10455355681176457663, - "venv/lib/python3.13/site-packages/greenlet/greenlet_slp_switch.hpp": 1694714764745718414, - "venv/lib/python3.13/site-packages/PIL/_util.py": 17116054813914373606, - "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.cpython-313-x86_64-linux-gnu.so": 6157288238855118017, - "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____getTaskForResource.html": 14513402972391639964, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.pyi": 17469792319379909312, - "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/names.py": 705261158040926128, - "docs/api/html/group__TelegramProvider.html": 2082762596768250446, - "venv/lib/python3.13/site-packages/git/db.py": 6680123567509774609, - "frontend/src/components/git/DeploymentModal.svelte": 3761818357348583741, - "frontend/vite.config.js": 3769019895915732131, - "venv/lib/python3.13/site-packages/numpy/lib/format.pyi": 11752225430594536662, - "venv/lib/python3.13/site-packages/fastapi/middleware/httpsredirect.py": 5775982317459922443, - "backend/tests/test_smoke_app.py": 15091189356064171349, - "venv/lib/python3.13/site-packages/rsa/core.py": 13203947234024924497, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period_range.py": 9598872514187524706, - "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/RECORD": 7810411361439269754, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_get_set.py": 14375107704596905983, - "venv/lib/python3.13/site-packages/pandas/tests/base/common.py": 13335865314907577354, - "venv/lib/python3.13/site-packages/pygments/lexers/teal.py": 10004159499175716139, - "docs/api/html/group__MIN__MAX__TOKENS.html": 3548157067782585566, - "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/REQUESTED": 15130871412783076140, - "venv/lib/python3.13/site-packages/numpy/_core/_exceptions.py": 5570375156851166818, - "venv/lib/python3.13/site-packages/authlib-1.6.6.dist-info/WHEEL": 16784970174376303810, - "venv/lib/python3.13/site-packages/pandas/tests/extension/test_sparse.py": 14343335524508450058, - "docs/api/html/group__update__session.html": 14739091990972920218, - "venv/lib/python3.13/site-packages/pip/_internal/locations/_distutils.py": 13559457590971821771, - "venv/lib/python3.13/site-packages/yaml/dumper.py": 11376022992370902766, - "docs/api/html/group__SessionLocal.html": 7893226225561483539, - "docs/api/html/group__providers.html": 16950412038675555694, - "docs/api/html/group__GitModels.html": 8608208250530048906, - "venv/lib/python3.13/site-packages/apscheduler/triggers/base.py": 1170652622104554892, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/strings.pyi": 15361498560917514616, - "docs/api/html/group__UserCreate.html": 5250366849739606333, - "docs/api/html/group__persist__tasks.html": 11196063061437802602, - "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/INSTALLER": 17282701611721059870, - "docs/api/html/group__ValidationFindingDto.html": 9426027159119514734, - "docs/api/html/group__TranslationExecutor.html": 4954564748951736491, - "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.asm": 16630507336149184509, - "docs/api/html/group__LoginE2E.html": 7883569100009245167, - "docs/api/html/group__test__calculate__schedule__very__small__window.html": 11155677236354096772, - "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_libfrequencies.py": 16302676625743291594, - "docs/api/html/group__GitServiceGithubMixin.html": 6075588890758025132, - "scripts/generate_financial_arrears_data.py": 15989529185799374517, - "docs/api/html/group__TaskCleanupService.html": 1939137499434761984, - "venv/lib/python3.13/site-packages/psycopg2/_json.py": 9729420372294281405, - "docs/api/html/group__APIClient______init____.html": 16760764157004706288, - "venv/lib/python3.13/site-packages/rapidfuzz/distance/Hamming.py": 3762934316884129514, - "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_file_handling.py": 732772718210067599, - "docs/api/html/dir_49e56c817e5e54854c35e136979f97ca.js": 3785536190143816087, - "venv/lib/python3.13/site-packages/pygments/sphinxext.py": 2768161754523156130, - "venv/lib/python3.13/site-packages/numpy/ma/core.py": 338350463523585845, - "docs/api/html/group__LanguageDetectServiceTests.html": 7434991135670181721, - "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_infer_objects.py": 6769980076977692890, - "docs/api/html/group__publication__repository.html": 13260610346382951650, - "docs/api/html/group__Schedule.html": 13333689163022748798, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_period.py": 10309599277099605395, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_easter.py": 3397863170068576196, - "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval.py": 8549680643865283913, - "docs/api/html/dir_49e56c817e5e54854c35e136979f97ca.html": 1429166617003811202, - "venv/lib/python3.13/site-packages/pydantic/_internal/_dataclasses.py": 15256193972947054890, - "venv/lib/python3.13/site-packages/pygments/__init__.py": 14493583771211110135, - "docs/api/html/group__DbMock.html": 17152034194564155607, - "venv/lib/python3.13/site-packages/pandas/tests/api/test_types.py": 8889829773923216769, - "venv/lib/python3.13/site-packages/pandas/tests/extension/base/ops.py": 13451323660386712332, - "venv/lib/python3.13/site-packages/uvicorn/_types.py": 3339389597716220585, - "venv/lib/python3.13/site-packages/pygments/lexers/elm.py": 14683437310498104916, - "venv/lib/python3.13/site-packages/httpx/_models.py": 6516278895118988231, - "venv/lib/python3.13/site-packages/pydantic/_internal/_signature.py": 17075003201648027250, - "backend/alembic/versions/2a7b8c9d0e1f_add_target_languages_to_translation_jobs.py": 7201017059554918882, - "docs/api/html/group__get__param.html": 2682553762034055515, - "docs/api/html/group__AssistantConfirmationRecord.html": 3562498999897451626, - "venv/lib/python3.13/site-packages/passlib/tests/sample_config_1s.cfg": 1809713544666311828, - "docs/api/html/group____validate__bcp47.html": 12782097460405837198, - "backend/src/services/clean_release/stages/__init__.py": 2966227562547886302, - "venv/lib/python3.13/site-packages/numpy/testing/_private/utils.py": 3646474165295296669, - "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_scalar_compat.py": 10534925655070162459, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_apply.py": 11248068903302454630, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/__init__.py": 8093984834068317399, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/matrix.pyi": 15667024840812783911, - "venv/lib/python3.13/site-packages/pygments/lexers/ecl.py": 12886695201994337568, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/panel.py": 17832701246248000383, - "venv/lib/python3.13/site-packages/pydantic/alias_generators.py": 5991580234179623842, - "docs/api/html/group__test__calculate__schedule__midnight__crossing.html": 6782803328672454391, - "venv/lib/python3.13/site-packages/numpy/__init__.pxd": 13527602429695748857, - "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_fiscal.py": 14652343698825214870, - "venv/lib/python3.13/site-packages/tzlocal/win32.py": 1949985224569608978, - "docs/api/html/group____policy__to__response.html": 1559230310925853956, - "venv/lib/python3.13/site-packages/referencing-0.37.0.dist-info/WHEEL": 2357997949040430835, - "venv/lib/python3.13/site-packages/pydantic/_internal/_core_utils.py": 13248004771466837645, - "venv/lib/python3.13/site-packages/pandas/core/dtypes/api.py": 9829629144629841277, - "docs/api/html/group__getADGroupMappings__Function.html": 940667681674860937, - "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/refresh_token.py": 17927903035152872556, - "venv/lib/python3.13/site-packages/PIL/__main__.py": 827665918723937817, - "venv/lib/python3.13/site-packages/git/objects/util.py": 691018806932355307, - "venv/lib/python3.13/site-packages/pygments/lexers/ride.py": 14096748896451437594, - "venv/lib/python3.13/site-packages/pandas/_libs/ops.cpython-313-x86_64-linux-gnu.so": 10490009128553397250, - "docs/api/html/globals_func_n.html": 12757886206585188105, - "venv/lib/python3.13/site-packages/attr/__init__.py": 14436902698244718082, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_windows.py": 9171347078900057357, - "docs/api/html/group__test__cot__json__formatter__plain__message.html": 6324270239309032244, - "venv/lib/python3.13/site-packages/numpy/lib/tests/test_packbits.py": 7592956764084658562, - "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_numba.py": 8250377774229977284, - "backend/src/services/git/_remote_providers.py": 7497094824437995253, - "backend/git_repos/remote/test-repo.git/objects/5a/82ef700fef88b69cd480e4025283809496ee20": 7003383965314354150, - "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/RECORD": 7844752443022514265, - "venv/lib/python3.13/site-packages/numpy/lib/_version.py": 15294708923334674060, - "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_user_array.py": 8711697474193296376, - "venv/lib/python3.13/site-packages/pip/_vendor/requests/auth.py": 912521029864094489, - "venv/lib/python3.13/site-packages/rsa/asn1.py": 3861968161559322784, - "venv/lib/python3.13/site-packages/pip/_vendor/rich/live.py": 8586058300967410643, - "docs/api/html/group____get__orchestrator.html": 13890254604779025703, - "docs/api/html/group__StorageService.html": 4742624646378331494, - "venv/lib/python3.13/site-packages/pandas/_libs/properties.cpython-313-x86_64-linux-gnu.so": 7092136701092358552, - "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_inclusive.py": 1389676378885557519, - "backend/alembic/env.py": 7020161137665650592, - "venv/lib/python3.13/site-packages/pygments/lexers/bare.py": 18266858932333619451, - "docs/api/html/group__compliance__repository.html": 5329437978880410491, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadSettings.html": 10425411179983087921, + "docs/api/html/group__test__get__detector__cache.html": 7969731651409649319, + "venv/lib/python3.13/site-packages/pandas/_libs/reshape.cpython-313-x86_64-linux-gnu.so": 10639750501333255943, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_mask.py": 9654897363729130542, + "docs/api/html/globals_func_p.html": 6858706269345690058, + "docs/api/html/group__handleGenerateDocs__Function.html": 10904959743671367375, + "docs/api/html/group__ui__route.html": 955048899493840648, + "docs/api/html/group__TestCleanReleaseApi.html": 16185754403987215515, + "frontend/src/lib/components/translate/BulkCorrectionSidebar.svelte": 5121541182559165710, + "docs/api/html/group__update__field__semantic.html": 3989002979285594854, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_msvc.h": 14580207065003376324, + "venv/lib/python3.13/site-packages/h11/_receivebuffer.py": 4631433251046338381, + "docs/api/html/tab_s.png": 9983007352471920092, + "docs/api/html/group__PreparePreviewResult.html": 13180386632469700647, + "docs/api/html/group__call__llm.html": 18394029930905917337, + "docs/api/html/group__MappingModels.html": 5347446187914937942, + "frontend/build/index.html": 6752566503479837061, + "docs/api/html/group__hasPermission__Function.html": 14331056000616172836, + "venv/lib/python3.13/site-packages/PIL/FontFile.py": 112053841436523305, + "docker/backend.Dockerfile": 8388271194100933486, + "docs/api/html/group__SupersetCompilationAdapter____supports__client__method.html": 15057319214542689475, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/test_support.pyi": 3893434234020223151, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/test_union_categoricals.py": 6843679254735807, + "docs/api/html/group__DatasetReviewSemanticModels.html": 9045404324038154150, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_count.py": 9186538441380080087, + "docs/api/html/group__AssistantLlmPlannerIntent.html": 17231865817068748918, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/util.py": 4815334059917765852, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/__init__.py": 1856412417337875350, + "venv/lib/python3.13/site-packages/pygments/lexers/_googlesql_builtins.py": 12251289884604745945, + "venv/lib/python3.13/site-packages/pandas/core/computation/pytables.py": 16326122536715961388, + "venv/lib/python3.13/site-packages/pandas/core/generic.py": 10174952306641057447, + "frontend/src/lib/components/assistant/__tests__/assistant_first_message.integration.test.js": 7546622841909210622, + "docs/api/html/group__encodeStoragePath__Function.html": 13436882047096086584, + "docs/api/html/group__ValidationService.html": 18408866395625101302, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_indexing_slow.py": 12558235638488427077, + "docs/api/html/group__test__component__exists.html": 8771500170212558619, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py": 10843527833285941674, + "docs/api/html/group__register__candidate__v2__endpoint.html": 5175001187205682785, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_business_month.py": 8485374160446988786, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_compare.py": 3079569478684646491, + "docs/api/html/group__test__get__report__not__found__returns__404.html": 6644055404803276631, + "venv/lib/python3.13/site-packages/pydantic_settings-2.13.0.dist-info/licenses/LICENSE": 16147999986886816451, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply_relabeling.py": 3930092097678339743, + "docs/api/html/group__test__update__git__config__raises__404__if__not__found.html": 17378932393540768304, + "docs/api/html/group__PasswordPrompt.html": 16889108354807031960, + "docs/api/html/group____recover__truncated__rows.html": 4179907453973361909, + "venv/lib/python3.13/site-packages/httpx/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_formats.py": 2743326139283991615, + "backend/src/plugins/translate/preview_prompt_builder.py": 15837150631111510351, + "backend/tests/test_models.py": 14571471179954305607, + "venv/lib/python3.13/site-packages/websockets/legacy/client.py": 4032878587330592478, + "venv/lib/python3.13/site-packages/cryptography/exceptions.py": 5173540025160237758, + "venv/lib/python3.13/site-packages/numpy/typing/__init__.py": 3608665862759745048, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6750/__init__.py": 11526998905763262175, + "venv/lib/python3.13/site-packages/pyasn1/type/constraint.py": 16776615503418773283, + "docs/api/html/group__TemplateVariableDto.html": 25487742519419104, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/test_insert.py": 9195027887301180927, + "docs/api/html/group__LLMAnalysisScheduler.html": 3957615184107530467, + "frontend/e2e/tests/translation.e2e.js": 9863246540495731784, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/events.py": 10224214051422678828, + "venv/lib/python3.13/site-packages/urllib3/contrib/pyopenssl.py": 11839715589709210412, + "docs/api/html/group____handle__unexpected__git__route__error.html": 4213682782631891407, + "venv/lib/python3.13/site-packages/numpy/lib/_nanfunctions_impl.pyi": 12566839271245057531, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/wsproto_impl.py": 3072714633272661847, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/LICENSE": 16174281248426886206, + "venv/lib/python3.13/site-packages/pip/_internal/req/req_set.py": 534498057184394558, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/test_str_accessor.py": 4066094997616994612, + "docs/api/html/group__Select.html": 13106318215518617964, + "backend/alembic/versions/e5f4d3c2b1a_add_policy_id_to_validation_results.py": 17469673353742094866, + "docs/api/html/group__storage__routes.html": 18273179632379125125, + "docs/api/html/group____normalize__object__payload.html": 14403994834116059510, + "docs/api/html/group__PluginLoader.html": 7483612115422215733, + "docs/api/html/group__test__api__correction__missing__dict.html": 16638106162967496087, + "docs/api/html/group__buildSidebarSections__Function.html": 528946104052993964, + "venv/lib/python3.13/site-packages/cryptography/x509/ocsp.py": 17973800752927680404, + "venv/lib/python3.13/site-packages/pip/_internal/metadata/importlib/_dists.py": 16746363359320577809, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polybase.pyi": 3404315099794022355, + "docs/api/html/search/all_11.js": 4961152569590003385, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_numpy_compat.py": 9112925138440787555, + "venv/lib/python3.13/site-packages/gitdb/test/test_pack.py": 7518388297247535189, + "docs/api/html/group__DashboardsApiTests.html": 1833223301110054529, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/py.typed": 15130871412783076140, + "docs/api/html/group__test__patch__profile__preferences__cross__user__denied.html": 11110530851215495265, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_arrayobject.py": 9578555381494297966, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/modules.pyi": 131293606732000587, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_equals.py": 18261810467289276844, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_period.py": 6047041810867211174, + "venv/lib/python3.13/site-packages/PIL/Image.py": 13615925856053031482, + "docs/api/html/group__test__connection.html": 1381382283167612811, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta.py": 7742506012012398830, + "backend/tests/test_smoke_plugins.py": 16443453635795704859, + "venv/lib/python3.13/site-packages/PIL/_imagingmorph.cpython-313-x86_64-linux-gnu.so": 11171810899664471137, + "docs/api/html/group__TestDictionaryFilter.html": 24770183405155834, + "docs/api/html/group__MigrationE2E.html": 11325763991415698239, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_setitem.py": 2059440410065814114, + "venv/lib/python3.13/site-packages/numpy/_globals.pyi": 12839879732484756979, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py": 9888795404090331319, + "docs/api/html/group__test__get__merge__status__returns__service__payload.html": 5423320726769507497, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE.BSD": 16833702494864671773, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_arithmetic.py": 15071337112968886738, + "frontend/src/routes/migration/__tests__/migration_dashboard.ux.test.js": 3945790406898769965, + "venv/lib/python3.13/site-packages/pandas/core/accessor.py": 1188716996113179524, + "venv/lib/python3.13/site-packages/pandas/_libs/tslib.pyi": 16170606956605100142, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/memmap.pyi": 2834143910417841603, + "docs/api/html/group__delete__conversation.html": 12285162829273366992, + "docs/api/html/group__ClarificationAnswerDto.html": 15559720501743641230, + "venv/lib/python3.13/site-packages/greenlet/tests/test_cpp.py": 10982107081267457286, + "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.pyi": 9243829615414967057, + "venv/lib/python3.13/site-packages/websockets/asyncio/connection.py": 17044409627448400723, + "backend/src/services/dataset_review/orchestrator_pkg/_helpers.py": 14441258905942300095, + "docs/api/html/group__setAssistantSeedMessage__Function.html": 13104263220941185944, + "venv/lib/python3.13/site-packages/pip/_vendor/resolvelib/py.typed": 15130871412783076140, + "docs/api/html/group__delete__git__config.html": 13270580245597492214, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_size.py": 16658280641516916790, + "docs/api/html/group__test__should__log__task__level.html": 11553737108336190998, + "docs/api/html/group__test__acceptPreview__Function.html": 12813709054328235897, + "venv/lib/python3.13/site-packages/ecdsa/eddsa.py": 15966858338456571506, + "venv/lib/python3.13/site-packages/pandas/core/series.py": 11618272682977710696, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_nanfunctions.py": 2356468092336024365, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/authorization_server.py": 13696038495666024258, + "venv/lib/python3.13/site-packages/sqlalchemy/util/typing.py": 17638587764345605306, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_numeric.py": 9417128508433693658, + "venv/lib/python3.13/site-packages/pluggy-1.6.0.dist-info/METADATA": 13695483537390128881, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_ujson.py": 12184194085993665945, + "docs/api/html/group__MigrationArchiveParser.html": 5158138547033150024, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_setops.py": 13440916133402971190, + "docs/api/html/group__build__namespaced__id.html": 12150766799543768530, "venv/lib/python3.13/site-packages/numpy/typing/mypy_plugin.py": 1306129775649200113, - "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_results.py": 18145930285449734975, - "venv/lib/python3.13/site-packages/pygments/lexers/eiffel.py": 8964563077187051946, - "docs/api/html/group__DatasetReviewApi__Module.html": 3012825213898058146, - "venv/lib/python3.13/site-packages/gitdb/exc.py": 13975852664048821278, - "frontend/src/lib/components/health/PolicyForm.svelte": 17995152707951193436, - "venv/lib/python3.13/site-packages/numpy/_utils/_inspect.pyi": 17825266810219495672, - "docs/api/html/group__test__map__columns__missing__database__id.html": 13354166006915808905, - "venv/lib/python3.13/site-packages/jsonschema/_format.py": 8987698648383779883, - "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_macos.py": 7028037591427052891, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_modules.py": 16835987029123241300, + "venv/lib/python3.13/site-packages/numpy/_core/numeric.pyi": 17785360553418386488, + "venv/lib/python3.13/site-packages/jaraco.classes-3.4.0.dist-info/LICENSE": 16343563559291870811, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/etcd.py": 4078354986605069279, + "frontend/build/_app/immutable/chunks/C3G398Gv.js": 15950703694744009907, + "venv/lib/python3.13/site-packages/starlette/staticfiles.py": 11256974306202558529, + "venv/lib/python3.13/site-packages/pygments/lexers/sophia.py": 15971500155124949058, + "docs/api/html/group____get__owned__field__or__404.html": 7076759849479749348, + "venv/lib/python3.13/site-packages/pygments/lexers/unicon.py": 1155454077052833821, + "docs/api/html/group__PermissionsModule.html": 6386249265249672269, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion": 2607512361823362698, + "docs/api/html/group__DashboardListingRoutes.html": 4195282586959114740, + "backend/src/services/clean_release/manifest_builder.py": 16367921257027024830, + "venv/lib/python3.13/site-packages/numpy/lib/_arrayterator_impl.pyi": 2744889206277418389, + "docs/api/html/group__SupersetContextRecoveryMixin.html": 3332182683532590481, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_dml_constructors.py": 4497038002189188311, + "backend/src/services/clean_release/compliance_execution_service.py": 11572933296960110695, + "docs/api/html/search/functions_6.js": 16111349203442418291, + "docs/api/html/group__ResourceSourceEntry.html": 8011516002332864562, + "docs/api/html/group__MaintenanceTaskResult.html": 15638476531187491715, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_win32_console.py": 18092547253031555768, + "docs/api/html/group__logger.html": 15165757884259946522, + "docs/api/html/group__ValidationTaskResponse.html": 10449136656682527650, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/poly1305.pyi": 14119032813912827271, + "venv/lib/python3.13/site-packages/requests/auth.py": 912521029864094489, + "docs/api/html/search/groups_f.js": 14632289900069259892, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_polynomial.py": 3049262771098624825, + "venv/lib/python3.13/site-packages/starlette/datastructures.py": 12097180838087229563, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.pyi": 15420142722808118826, + "venv/lib/python3.13/site-packages/pandas/tests/frame/common.py": 7426635866622531875, + "venv/lib/python3.13/site-packages/passlib/tests/test_handlers_cisco.py": 1786475632318043669, + "venv/lib/python3.13/site-packages/sqlalchemy/util/__init__.py": 15821192733097665649, + "venv/lib/python3.13/site-packages/pygments/lexers/spice.py": 1376126320935115350, + "backend/src/core/middleware/trace.py": 10060731108870842086, + "backend/src/api/routes/__tests__/test_clean_release_api.py": 8153144254371183034, + "venv/lib/python3.13/site-packages/pandas/core/tools/numeric.py": 8510988912136285022, + "backend/alembic/versions/a5b6c7d8e9f0_set_null_ondelete_for_task_records_env_fk.py": 12852400611000124919, + "docs/api/html/group__assistantChatStore__tests__Function.html": 434816894896782034, + "docs/api/html/group__stop.html": 16956058968433395845, + "docs/api/html/group__DictionaryManager.html": 10525609506852001360, + "venv/lib/python3.13/site-packages/pycparser/yacctab.py": 18018314512916353707, + "venv/lib/python3.13/site-packages/pydantic/_internal/_config.py": 12760390602912369447, + "docs/api/html/group__TestClickHouseTimestampNormalization.html": 16093633626248389892, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_transform.py": 5674175311567817854, + "backend/git_repos/remote/test-repo.git/hooks/pre-commit.sample": 102291230237676317, + "backend/src/core/auth/config.py": 205969007784846720, + "venv/lib/python3.13/site-packages/pandas/io/common.py": 12088195203620154171, + "docs/api/html/group__GitUtils.html": 17815241981515134154, + "venv/lib/python3.13/site-packages/greenlet/greenlet_thread_support.hpp": 7773996382104849037, + "docs/api/html/group__test__validation__policy__create__rejects__legacy__string__custom__channels.html": 14661357931736830458, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_direct.py": 12680113511641199070, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_groupby.py": 12709199432838648700, + "venv/lib/python3.13/site-packages/pygments/lexers/freefem.py": 9851243865301700231, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_iat.py": 3570464615146398178, + "venv/lib/python3.13/site-packages/pygments/lexers/numbair.py": 17382646010399794697, + "backend/src/scripts/delete_running_tasks.py": 4209804708500252012, + "docs/api/html/group__read__users__me.html": 12849277501505045646, + "venv/lib/python3.13/site-packages/pandas/core/arrays/arrow/_arrow_utils.py": 17808805047762566525, + "venv/lib/python3.13/site-packages/jsonschema/tests/fuzz_validate.py": 2328585939822059352, + "docs/api/html/group__InternalSourcesOnlyStage.html": 4233027249297243678, + "docs/api/html/group__MigrationEngine.html": 9187249700735075008, + "frontend/build/_app/immutable/chunks/DfRyuIRe.js": 2843700033277271727, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_accessor.py": 10074496835594855786, + "venv/lib/python3.13/site-packages/pandas/tests/base/common.py": 13335865314907577354, + "docs/api/html/group__AuthSchemas.html": 10571020082050700369, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_cli.py": 3173259257428855556, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_pct_change.py": 18056235438452125034, + "venv/lib/python3.13/site-packages/PIL/report.py": 9428754440615681790, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/LICENSE": 14599279238716790189, + "docs/api/html/navtreeindex1.js": 11503108132424855366, + "docs/api/html/group__test__transform__yaml__replaces__database__uuid.html": 906181308081154952, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7519/jwt.py": 9642615960197748105, + "docs/api/html/group__frontend__src__lib__i18n__index__ts______.html": 15852621729962062852, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_base.py": 15292769510056833117, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/einsumfunc.pyi": 200771351328522379, + "docs/api/html/group__fetchData__Function.html": 12479170828711469156, + "frontend/src/routes/settings/LlmSettings.svelte": 10881646244972773692, + "venv/lib/python3.13/site-packages/numpy/f2py/cb_rules.pyi": 12275242654430570028, + "venv/lib/python3.13/site-packages/urllib3/util/proxy.py": 17667887856427739069, + "venv/lib/python3.13/site-packages/numpy/char/__init__.py": 4428218097234308768, + "docs/api/html/group__activity__Store.html": 10103402141887655434, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/METADATA": 5628172948035858892, + "backend/src/api/routes/__tests__/test_reports_detail_api.py": 13487808524141158988, + "venv/lib/python3.13/site-packages/authlib/common/errors.py": 16082974228565515177, + "docs/api/html/group__delete__gitea__repository.html": 15330998186210577395, + "docs/api/html/group__HealthMatrix.html": 10134874641663847032, + "docs/api/html/group__test__missing__variables.html": 14137199728172789432, + "docs/api/html/group__test__git__service__configure__identity__updates__repo__local__config.html": 5513064374674137589, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/__version__.py": 14138134075118474134, + "venv/lib/python3.13/site-packages/pandas/core/arrays/boolean.py": 8821671562937866269, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_records.py": 6252761812228230408, + "frontend/build/_app/immutable/nodes/14.BQTiej3V.js": 4128737585340280513, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____closeDrawer.html": 8754263409785426495, + "docs/api/html/group__test__save__config__accepts__raw__payload__and__keeps__extras.html": 11846202691943319524, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7523/auth.py": 13542332344403032059, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/processors.py": 4817637175145012571, + "docs/api/html/group__SupersetContextExtractorPII.html": 8059785191565221125, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_matplotlib.py": 12775850858284584296, + "venv/lib/python3.13/site-packages/pydantic/_internal/_generate_schema.py": 7343380095251236503, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/schema.py": 5391655516870611151, + "docs/api/html/group____plan__intent__with__llm.html": 11560361184946600835, + "venv/lib/python3.13/site-packages/pydantic/deprecated/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/tests/test__all__.py": 10689265249302359388, + "docs/api/html/group__candidate__service.html": 9508293729016004404, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/config.py": 10707678943298497676, + "docs/api/html/group__TargetSchemaColumnInfo.html": 8163902877022443310, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/RECORD": 8971487317739420905, + "venv/lib/python3.13/site-packages/greenlet/tests/test_extension_interface.py": 11693583111513457974, + "venv/lib/python3.13/site-packages/PIL/QoiImagePlugin.py": 17663704273100331654, + "venv/lib/python3.13/site-packages/pandas/tests/config/test_localization.py": 9524481848419616725, + "docs/api/html/group____index__by__uuid.html": 12082993556480921250, + "venv/lib/python3.13/site-packages/greenlet/tests/test_leaks.py": 1219835696230215605, + "docs/api/html/group__DatasetPreview.html": 13745385565163960445, + "venv/lib/python3.13/site-packages/passlib/tests/sample1.cfg": 9597283806285213340, + "docs/api/html/group__frontend__src__lib__api__translate__target__schema__js____checkTargetTableSchema.html": 12580964374623879461, + "backend/src/api/routes/assistant/_tool_registry.py": 7301763389751164134, + "docs/api/html/group__handleManageSelected__Function.html": 6277194528006873152, + "docs/api/html/group__test__get__logs__by__task__id.html": 3774988735198312173, + "docs/api/html/group__MergeContinueRequest.html": 15873066823531580576, + "venv/lib/python3.13/site-packages/pygments/lexers/tls.py": 3552078393009383412, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/hmac.pyi": 9405122752084210696, + "venv/lib/python3.13/site-packages/websockets/headers.py": 13587146967490088657, + "docs/api/html/group____policy__to__response.html": 1559230310925853956, + "venv/lib/python3.13/site-packages/attr/filters.pyi": 13942974755090135724, + "docs/api/html/group__sync__permission__catalog.html": 4976963130726233994, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/__init__.py": 4390908730288159455, + "venv/lib/python3.13/site-packages/numpy/random/mtrand.pyi": 17144590106028661128, + "venv/lib/python3.13/site-packages/PIL/IcnsImagePlugin.py": 1870235606309784691, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/openssl/binding.py": 17078280153372356796, + "docs/api/html/group__StateMock.html": 14819043532305148003, + "backend/git_repos/remote/test-repo.git/objects/79/b070ad8af7bbc12169f7660d2a7a72ec7a889a": 8153595714065675791, + "docs/api/html/group__ReportCardTest__Module.html": 2682480122400206889, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/dictionary.py": 9599439750713575741, + "frontend/vitest.config.js": 4756170101280085905, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_format.py": 16150524965028369071, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/authorization_server.py": 724388765673082720, + "venv/lib/python3.13/site-packages/jeepney/tests/test_auth.py": 15125055415702968414, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_integer/foo90.f90": 3492659529812762861, + "docs/api/html/group__CleanReleaseAuditLog.html": 13803004817531190907, + "venv/lib/python3.13/site-packages/jose/utils.py": 1926138853970725083, + "venv/lib/python3.13/site-packages/pygments/lexers/actionscript.py": 8829560731160428621, + "venv/lib/python3.13/site-packages/PIL/ImageChops.py": 16368812693381154921, + "docs/api/html/group__test__trigger__run__success.html": 2545532746329268480, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_dict.py": 808180749933361743, + "venv/lib/python3.13/site-packages/uvicorn/protocols/websockets/auto.py": 12818217385551437286, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/resample/__init__.py": 15130871412783076140, + "backend/src/api/routes/assistant/_llm_planner.py": 16869011577670774026, + "backend/src/core/__init__.py": 8673978627093182792, + "docs/api/html/group__push__span.html": 6501660690027552485, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_slice.py": 9936085512921529417, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_elements_constructors.py": 9279447066034364952, + "docs/api/html/group____sync__environment__records.html": 7144329195517588362, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/structures.py": 2668010839316715865, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/__main__.py": 11917862648584782420, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/__init__.py": 15130871412783076140, + "docs/api/html/group__TestValidatePath.html": 12521153993368156395, + "docs/api/html/group__Role.html": 11260980512040554350, + "frontend/src/routes/settings/MigrationSettings.svelte": 12844990967512238268, + "frontend/build/_app/immutable/chunks/-sQJ30y6.js": 3131847702071268627, + "venv/lib/python3.13/site-packages/keyring/backends/macOS/api.py": 4502902558534812719, + "venv/lib/python3.13/site-packages/pydantic/utils.py": 1533455264689378477, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_amd64_unix.h": 16679226724722970205, + "docs/api/html/group__SupersetCompilationAdapter____request__sql__lab__session.html": 2880186868861912390, + "docs/api/html/group____require__fernet__key.html": 5961515902811999476, + "frontend/src/lib/components/translate/TargetSchemaHint.svelte": 9354096644840520484, + "venv/lib/python3.13/site-packages/pandas/core/reshape/merge.py": 300205528811693975, + "docs/api/html/group__UtilsModule.html": 2939005577247546143, + "docs/api/html/group__TestAssistantAuthz.html": 3742284651131782250, + "docs/api/html/group__CleanReleaseRepository.html": 12202309648342354068, + "docs/api/html/group__test__get__run__detail__success.html": 7842784758779741648, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_arrow.py": 10068866425352207113, + "venv/lib/python3.13/site-packages/pygments/lexers/gcodelexer.py": 13712494327007684318, + "docs/api/html/group__test__get__dashboards__profile__filter__matches__owner__object__payload__contract.html": 13048004686797783316, + "venv/lib/python3.13/site-packages/passlib/handlers/ldap_digests.py": 8658529184965699766, + "venv/lib/python3.13/site-packages/pandas/core/ops/dispatch.py": 7041711677358147534, + "venv/lib/python3.13/site-packages/numpy/_core/lib/npy-pkg-config/mlib.ini": 16151597765994697599, + "backend/src/core/task_manager/__tests__/test_context.py": 9922342013893651977, + "docs/api/html/search/mag_d.svg": 14208504129760637316, + "venv/lib/python3.13/site-packages/pandas/_libs/sparse.pyi": 12526865400413362140, + "venv/lib/python3.13/site-packages/dateutil/tz/tz.py": 5258008267965780817, + "venv/lib/python3.13/site-packages/keyring/credentials.py": 14515496037842724455, + "docs/api/html/group____to__preference__payload.html": 10972305312357222563, + "venv/lib/python3.13/site-packages/pydantic/v1/dataclasses.py": 7833468513057242440, + "docs/api/html/globals_l.html": 8615069986131999743, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_round.py": 9358309072340573675, + "docs/api/html/group__SupersetDatabasesMixin.html": 296914084518346512, + "backend/tests/test_sql_table_extractor.py": 183899682433347226, + "docs/api/html/group__cot__trace__context.html": 7845040058445266458, + "docs/api/html/group__SupersetParsedContext.html": 16475087038796278202, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_feather.py": 13613400750755530364, + "venv/lib/python3.13/site-packages/pip/_vendor/distro/py.typed": 15130871412783076140, + "docs/api/html/group____apply__dashboard__filters.html": 13208356314740961922, + "docs/api/html/group__revoke__api__key.html": 5362512618088609082, + "venv/lib/python3.13/site-packages/six.py": 948867418687428421, + "venv/lib/python3.13/site-packages/pandas/tests/window/moments/test_moments_consistency_ewm.py": 12153690217678546798, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_einsum.py": 17286384318722237180, + "venv/lib/python3.13/site-packages/fastapi/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/jaraco/classes/properties.py": 6976928770004942858, + "docs/api/html/group__check__api__key__environment__scope.html": 12869540980840565823, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_delitem.py": 16538190861492829338, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/polynomial_polyutils.pyi": 3841660741524427413, + "docs/api/html/group__loadData__Function.html": 8139473336432513519, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft_umath.cpython-313-x86_64-linux-gnu.so": 16443444849116065961, + "docs/api/html/group__IdMappingService.html": 128548327933737637, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_drop.py": 15751236355035730554, + "venv/bin/pip3": 13861749540792881808, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/formatters/__init__.py": 3247870839323520754, + "venv/lib/python3.13/site-packages/pandas/tests/frame/conftest.py": 15090201792072045503, + "docs/api/html/group__orchestrator__sql__rows.html": 11564020210274340689, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_os.h": 7883047506449405400, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_unary.py": 7128845965976836334, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py": 10367325397209016951, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/heuristics.py": 1422768761337195135, + "docs/api/html/group__test__legacy__checks__endpoints__still__available.html": 6527479084678491892, + "venv/lib/python3.13/site-packages/numpy/polynomial/_polytypes.pyi": 7528777458251688517, + "frontend/src/components/Navbar.svelte": 2640465687592983765, + "backend/src/services/reports/report_service.py": 9387534669439738529, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_mem_overlap.py": 4202716587079637350, + "venv/lib/python3.13/site-packages/requests/packages.py": 7733041271118298220, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py": 16463243654123731993, + "docs/api/html/group__frontend__src__services__toolsService__js____getTaskStatus.html": 2867392422291554776, + "docs/api/html/group__TranslateTargetSchemaApi.html": 13948109670112640865, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7592/__init__.py": 1398617034174236358, + "docs/api/html/group__TraceContextMiddleware.html": 8346923034582577387, + "docs/api/html/group__PreviewExecutor.html": 5786108329700439190, + "venv/lib/python3.13/site-packages/pandas/tests/reductions/test_stat_reductions.py": 11569525436540769152, + "venv/lib/python3.13/site-packages/gitdb/test/lib.py": 13294941660283059872, + "docs/api/html/group__GitWorkspacePanel.html": 15537793348093112183, + "docs/api/html/group__DashboardHub__getSortIndicator__Function.html": 2447268847448159799, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/fields.cpython-313-x86_64-linux-gnu.so": 16927263232848238698, + "venv/lib/python3.13/site-packages/pydantic/version.py": 3699343596421001359, + "venv/lib/python3.13/site-packages/pip/_internal/cli/main_parser.py": 7942770969280622910, + "backend/src/services/clean_release/repositories/report_repository.py": 8327339942479580975, + "docs/api/html/group__SupersetClientExtractCompiledSqlFromPreviewResponse.html": 699068057352781572, + "venv/lib/python3.13/site-packages/referencing/jsonschema.py": 14596871117484757502, + "docs/api/html/group__test__resolve__namespace__separates__demo__and__real.html": 7744970425001491248, + "docs/api/html/group__MaintenanceDashboardBanner.html": 13827348584356629256, + "docs/api/html/group__TranslateRunListRoutesModule.html": 7983808577352812807, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/pkcs12.pyi": 7801211423226951223, + "backend/git_repos/remote/test-repo.git/objects/3e/05ec6ef6504483c4bc7d00f0d0272c38ec661f": 11182016264585388842, + "venv/lib/python3.13/site-packages/pyasn1/codec/cer/decoder.py": 17559787396729804535, + "venv/lib/python3.13/site-packages/numpy/_core/__init__.pyi": 2783897020308040960, + "docs/api/html/group__test__dashboards__api.html": 3569185305104571400, + "venv/lib/python3.13/site-packages/PIL/BdfFontFile.py": 10811640174253687845, + "docs/api/html/group__FilterSource.html": 17708105643619200082, + "venv/lib/python3.13/site-packages/pygments/lexers/python.py": 8995519134978505744, + "docs/api/html/group__MaintenanceEndTaskResult.html": 18181177616321427742, + "frontend/src/components/git/GitWorkspacePanel.svelte": 15166222911685720412, + "venv/lib/python3.13/site-packages/pygments/lexers/scdoc.py": 8481749542463754279, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_umath_accuracy.py": 3904008607472505376, + "venv/lib/python3.13/site-packages/pygments/lexers/rego.py": 16022844238903541738, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_generator_mt19937.py": 7299586490442539723, + "docs/api/html/group__GitServerConfigBase.html": 14855349343114147443, + "venv/lib/python3.13/site-packages/anyio/streams/file.py": 15232345967545920863, + "docs/api/html/group__load__Function.html": 10190139410181623652, + "venv/lib/python3.13/site-packages/itsdangerous/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/lib/_format_impl.pyi": 15745512451486888537, + "venv/lib/python3.13/site-packages/numpy/_typing/_ufunc.py": 16859854811012717088, + "venv/lib/python3.13/site-packages/greenlet/PyGreenletUnswitchable.cpp": 3160733517193315684, + "docs/api/html/group____make__session.html": 3765523648902768678, + "venv/lib/python3.13/site-packages/numpy/lib/_scimath_impl.py": 14696841109142419143, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_datetimes.py": 7889573945417063183, + "venv/lib/python3.13/site-packages/uvicorn/_subprocess.py": 13965754246493254447, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/WHEEL": 13232065379159720345, + "frontend/build/_app/immutable/nodes/34.C2rJoWaW.js": 17388794405887370476, + "frontend/test-results/live-project-check.e2e.js--abfe5-project-with-admin-admin123-chromium/test-failed-1.png": 4602930255871194331, + "venv/lib/python3.13/site-packages/PIL/features.py": 3481835375516797487, + "venv/lib/python3.13/site-packages/httpcore-1.0.9.dist-info/RECORD": 12015288851076448205, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalar_ctors.py": 631744056514965940, + "backend/tests/core/migration/test_archive_parser.py": 7294000985611119675, + "venv/lib/python3.13/site-packages/more_itertools/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/anyio/_core/_asyncio_selector_thread.py": 1760597191437520000, + "backend/src/core/superset_client/_dashboards_crud.py": 8682388093872211883, + "docs/api/html/group__LogFilterBar.html": 6090838125002308969, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/syntax.py": 18070449217682341495, + "venv/lib/python3.13/site-packages/rpds/__init__.py": 5862197328247012275, + "venv/lib/python3.13/site-packages/pandas/_libs/sparse.cpython-313-x86_64-linux-gnu.so": 2051484129454294720, + "docs/api/html/group__CreateComplianceRunRequest.html": 8446298024144622339, + "docs/api/html/group__version.html": 2015740676777523593, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_cumulative.py": 15644124412739892167, + "docs/api/html/group__auth__config.html": 14653694386669705505, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/rsa.py": 5392922995791487437, + "venv/lib/python3.13/site-packages/pandas/_libs/missing.pyi": 13543139288840944411, + "docs/api/html/group__test__candidate__registration__contract.html": 1989144001943621829, + "docs/api/html/group__FieldSemanticUpdateRequest.html": 7457072171844274462, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein_py.py": 1933257612725836484, + "frontend/e2e/helpers/api.helper.js": 12740720187461301874, + "docs/api/html/group__ReportDetailView.html": 2581277258940002250, + "frontend/src/lib/components/assistant/AssistantChatPanel.svelte": 5882800317154842214, + "backend/src/api/routes/__init__.py": 4671856475376690434, + "venv/lib/python3.13/site-packages/anyio/_core/_resources.py": 8463932838064724564, + "docs/api/html/group__SupersetClientGetDatabase.html": 4802291779474339990, + "docs/api/html/group__LogStats.html": 908679196460419136, + "venv/lib/python3.13/site-packages/passlib/handlers/misc.py": 18255658377109333797, + "venv/lib/python3.13/site-packages/numpy/core/_internal.py": 14816989375912210674, + "docs/api/html/group__test__task__matches__dashboard__logic.html": 12048228571630804289, + "docs/api/html/group____ensure__user__dashboard__preferences__columns.html": 12772575671333756881, + "venv/lib/python3.13/site-packages/jeepney/io/trio.py": 11859775321483330469, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_select.py": 7083040959665006634, + "docs/api/html/group__WebSocketLogHandler__emit.html": 18196197399566537987, + "venv/lib/python3.13/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__test__persist__task__new.html": 15843661225530940640, + "docs/api/html/navtreeindex23.js": 17692914288402765747, + "backend/src/api/routes/assistant/_tool_search_dashboards.py": 14588777267223852707, + "backend/src/api/routes/translate/_run_list_routes.py": 11620203305409913352, + "dist/docker/ss-tools-backend.0.1.6.tar.xz": 7018813354616875801, + "docs/api/html/group__DashboardHub__doesDashboardPassColumnFilters__Function.html": 16470140080507162324, + "docs/api/html/group__build__risks.html": 288765426449485384, + "venv/lib/python3.13/site-packages/pip/_internal/commands/__init__.py": 3795218871212151351, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/response.py": 15004896908941411698, + "frontend/src/components/backups/BackupList.svelte": 7178110735645066783, + "backend/src/api/routes/dataset_review_pkg/_routes.py": 12316446040755204777, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/__init__.py": 3673322722277252961, + "venv/lib/python3.13/site-packages/itsdangerous/serializer.py": 14271229091461164933, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/arithmetic.pyi": 467927803068643024, + "venv/lib/python3.13/site-packages/gitdb/fun.py": 55226330810782184, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/groupby.py": 14661033005768429774, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/_cipheralgorithm.py": 10057637461647348368, + "docs/api/html/group__create__task.html": 5627761289813856584, + "docs/api/html/group__get__datasets.html": 5086430945264562862, + "docs/api/html/group__BuildOfflineDockerBundle.html": 2244041374994039539, + "venv/lib/python3.13/site-packages/pydantic/v1/parse.py": 5875950480538559709, + "docs/api/html/group__buildDatasetReviewRequestOptions__Function.html": 3581409884229047567, + "venv/lib/python3.13/site-packages/pandas/core/dtypes/concat.py": 14737724508458849795, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_style.py": 6849613206971398029, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_warnings.py": 13071210233876253749, + "docs/api/html/group__update__storage__settings.html": 5443519149117079992, + "docs/api/html/group__update__consolidated__settings.html": 10739551165144622798, + "docs/api/html/group__test__authenticate__user__updates__last__login.html": 681636108963024398, + "docs/api/html/group__test__extra__form__data__merge.html": 8128733170632705704, + "docs/api/html/group____admin__user.html": 6240483045330435769, + "merge_kilo.py": 4282227344165325430, + "venv/lib/python3.13/site-packages/pydantic/_internal/_utils.py": 10936299012115637790, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/arrayprint.pyi": 2424868797160722318, + "venv/lib/python3.13/site-packages/numpy/__init__.py": 2331213407497901634, + "docs/api/html/group__test__authenticate__inactive__user.html": 8193931683359814732, + "venv/lib/python3.13/site-packages/pygments/unistring.py": 18037463865539157041, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/INSTALLER": 17282701611721059870, + "backend/src/api/routes/admin_api_keys.py": 4969151877280913849, + "frontend/build/_app/immutable/chunks/DCWzHiHq.js": 3606617218252137111, + "backend/src/models/dataset_review_pkg/__init__.py": 1796062139548584878, + "docs/api/html/group__test__get__history__wraps__unexpected__error__as__500.html": 11007813825591521037, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/interval/test_interval.py": 8549680643865283913, + "docs/api/html/group__ClarificationQuestionPayload.html": 13225683527391113096, + "docs/api/html/group__ScheduleResponse.html": 597319143140494541, + "docs/api/html/group__get__repository__binding.html": 642256703362197453, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_update_delete.py": 1317337950621091021, + "docs/api/html/group__isDirectory__Function.html": 3452622402682011690, + "docs/api/html/group____ensure__dataset__review__session__columns.html": 16968513755674644702, + "docs/api/html/group__PreviewEnqueueResultResponse.html": 6184757346716551211, + "venv/lib/python3.13/site-packages/pandas/io/iceberg.py": 13137128885014736233, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_csv.py": 571991535656576735, + "venv/lib/python3.13/site-packages/pandas/core/indexes/base.py": 496956059910401121, + "venv/lib/python3.13/site-packages/dateutil/tz/__init__.py": 9617254832690762236, + "venv/lib/python3.13/site-packages/numpy/_core/function_base.py": 5676611841071163854, + "docs/api/html/group____init__session.html": 12506025499557216543, + "docs/api/html/group__venv__lib__python3__13__site__packages__greenlet__TThreadState__hpp____ThreadState.html": 17071810675918401264, + "docs/api/html/group__ColumnsTable.html": 8262379607630613277, + "docs/api/html/group__DocumentationPlugin.html": 4602413971047524450, + "docs/api/html/group__GitRepositorySchema.html": 13549814497751486913, + "docs/api/html/group__get__session__detail.html": 2777345426267776065, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/multiarray.py": 3224049574922696945, + "venv/lib/python3.13/site-packages/pandas/compat/__init__.py": 6077099417711209303, + "venv/lib/python3.13/site-packages/jsonschema/benchmarks/__init__.py": 5863110456909134678, + "docs/api/html/group__BackupTypes__Module.html": 6128924600139653510, + "docs/api/html/group__SupersetClientExtractUserDisplay.html": 5365697524336802629, + "venv/lib/python3.13/site-packages/starlette/applications.py": 13591448461234020757, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/session.py": 5126234270435358377, + "docs/api/html/group__MissingMappingModal.html": 3905855008524248166, + "venv/lib/python3.13/site-packages/git/refs/head.py": 16271128287606894569, + "venv/lib/python3.13/site-packages/apscheduler/executors/pool.py": 10344126569001032241, + "venv/lib/python3.13/site-packages/greenlet/tests/leakcheck.py": 10179165748538820557, + "docs/api/html/group__CorrectionConflictResult.html": 16510702604708035567, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_chaining_and_caching.py": 15814259766479305290, + "venv/lib/python3.13/site-packages/authlib/jose/rfc8037/okp_key.py": 10312614562914096039, + "frontend/src/lib/components/ui/DatabaseSearchCombobox.svelte": 13085059317395294999, + "docs/api/html/group__clean__db.html": 17613548754885493168, + "docs/api/html/group__has__permission.html": 1685491093213182094, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____stopPolling.html": 7926062796723513466, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs_scalars.py": 652332294655046106, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_arithmetic.py": 17043958604607035425, + "backend/alembic/versions/ed310b33f02c_multi_language_translation_tables.py": 13011907365627280179, + "docs/api/html/group__getReports__Function.html": 10989136831349411204, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/json.py": 15373167408891671190, + "docs/api/html/group__DatasetDetailResponse.html": 5915339838963904539, + "docs/api/html/group__get__report.html": 3004053766336257006, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/DamerauLevenshtein.pyi": 2700568594438441246, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/compiler.py": 13924519162400330589, + "docs/api/html/group__normalize__llm__settings.html": 8684454067738894675, + "backend/src/models/dashboard.py": 18145369015035676491, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reset_index.py": 5946666595195952693, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_timeseries_window.py": 1973365560758236222, + "venv/lib/python3.13/site-packages/PIL/PalmImagePlugin.py": 13008868719182630239, + "backend/git_repos/remote/test-repo.git/config": 14478893374498894766, + "docs/api/html/group__get__check__status.html": 4939629494472656154, + "docs/api/html/search/functions_5.js": 12994729569996994977, + "docs/api/html/group____mask__sensitive__text.html": 10707233070284578212, + "venv/lib/python3.13/site-packages/pillow.libs/liblcms2-cc10e42f.so.2.0.17": 4221600618602131476, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_array_interface.py": 13648006002292914247, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-arccos.csv": 3026156288992132911, + "frontend/build/_app/immutable/chunks/56mUezx2.js": 15653552307278920184, + "venv/lib/python3.13/site-packages/websockets/speedups.cpython-313-x86_64-linux-gnu.so": 10941658763526595989, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_join.py": 1131792466825856990, + "venv/lib/python3.13/site-packages/PIL/PcxImagePlugin.py": 1691405132377495336, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust.abi3.so": 2537152913308578487, + "docs/api/html/group__ADGroupMappingCreate.html": 332191690038373330, + "docs/api/html/group__test__role__permission__association.html": 3634921328837162404, + "venv/lib/python3.13/site-packages/numpy/f2py/func2subr.pyi": 16573082220135508510, + "backend/src/api/routes/assistant/_tool_llm_documentation.py": 3480953103839994874, + "venv/lib/python3.13/site-packages/websockets/sync/messages.py": 15699091325918681038, + "docs/api/html/group__BeliefFormatter.html": 1872767394370220168, + "docs/api/html/group__test__create__job__valid.html": 2035819506657562745, + "docs/api/html/group__AuthConfig.html": 8514504409080395912, + "docs/api/html/group__docker__nginx__ssl__conf.html": 6000110961620180556, + "docs/api/html/group__test__recover__imported__filters__reconciles__raw__native__filter__ids__to__metadata__names.html": 9441026336344613856, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_astype.py": 15384141282611151560, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_drop.py": 17940792593116540139, + "venv/lib/python3.13/site-packages/idna/py.typed": 15130871412783076140, + "backend/src/plugins/storage/plugin.py": 8516872254456553013, + "docs/api/html/group__SemanticCandidate.html": 16155484952491481356, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_repeat.py": 9155935990114743583, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/util.py": 4890614159013873000, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_numpy.py": 4073641912755651877, + "venv/lib/python3.13/site-packages/_pytest/pathlib.py": 9719734558816066453, + "venv/lib/python3.13/site-packages/pygments/lexers/praat.py": 54226477748516386, + "docs/api/html/group__frontend__src__services__storageService__js____uploadFile.html": 16382303397706812434, + "docs/api/html/group____detector__cache__key.html": 17353560702377924027, + "venv/lib/python3.13/site-packages/sqlalchemy/event/attr.py": 5971663429221923939, + "docs/api/html/group__ResourceSourceRegistry.html": 11897883287885038128, + "docs/api/html/group__user__roles.html": 5258273442326465678, + "venv/lib/python3.13/site-packages/pygments/lexers/capnproto.py": 15713281507099411738, + "backend/tests/services/dataset_review/test_superset_matrix.py": 3788494235626090289, + "docs/api/html/group__test__start__session__endpoint__returns__created__summary.html": 4072374425456734100, + "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.c": 2519219289444051739, + "docs/api/html/group__PromoteRequest.html": 11374219910371019333, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_arm64_masm.asm": 16630507336149184509, + "venv/lib/python3.13/site-packages/numpy/lib/_polynomial_impl.py": 12771179468704314203, + "backend/tests/test_security_orthogonal.py": 5653088445462853264, + "venv/lib/python3.13/site-packages/ecdsa/ecdh.py": 12596317261561755793, + "venv/lib/python3.13/site-packages/pandas/tests/frame/indexing/test_set_value.py": 17480833128128893268, + "venv/lib/python3.13/site-packages/uvicorn/middleware/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/packages/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/passlib/crypto/_md4.py": 379335134151814809, + "venv/lib/python3.13/site-packages/keyring/backend.py": 14185808762437650951, + "docs/api/html/group__DEFAULT__CONTEXT__WINDOW.html": 14468316197072801713, + "venv/lib/python3.13/site-packages/pygments/lexers/functional.py": 15896849233007178031, + "docs/api/html/group__get__run__status.html": 4185917444778756301, + "docs/api/html/group__loadRepositoryStatuses__Function.html": 16252993742945396544, + "venv/lib/python3.13/site-packages/jsonschema/__main__.py": 18136546137047911521, + "backend/src/api/routes/profile.py": 8666418382456481533, + "docs/api/html/group__AuthState__Interface.html": 8632817980324199273, + "dist/docker/ss-tools-backend.0.1.7.tar.xz": 15673571321704015687, + "venv/lib/python3.13/site-packages/pip/_internal/req/__init__.py": 12266077985828139928, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/markup.py": 5413800947725916538, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_clip.py": 11111183698178681331, + "venv/lib/python3.13/site-packages/pandas/io/formats/csvs.py": 17223598248259691292, + "backend/src/services/clean_release/__tests__/test_report_builder.py": 11856874434357958029, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/_py_util.py": 12836989066113400538, + "venv/lib/python3.13/site-packages/pygments/lexers/whiley.py": 218468511185731869, + "docs/api/html/group__WebSocketLogHandler__get__recent__logs.html": 5454880085918399099, + "frontend/playwright-report/index.html": 17750318162027456191, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.py": 1472021608501071055, + "venv/lib/python3.13/site-packages/pandas/tests/resample/test_period_index.py": 6634414375581820058, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_astype.py": 4975959187621745360, + "docs/api/html/group____extract__chart__uuids__from__archive.html": 5294683500183190826, + "dist/docker/ss-tools-0.1.8.bundle": 11104076744355433821, + "docs/api/html/group__SupersetClientUpdateDataset.html": 9267713141598532121, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_asyncio.py": 2820168745710754136, + "venv/lib/python3.13/site-packages/pygments/lexers/r.py": 14263133575134645005, + "docs/api/html/group____make__environment.html": 15812631705541506807, + "venv/lib/python3.13/site-packages/pydantic/v1/error_wrappers.py": 5701679786078971939, + "venv/lib/python3.13/site-packages/greenlet/PyGreenlet.hpp": 6366432579621298231, + "venv/lib/python3.13/site-packages/httpx/_config.py": 8800538050293498928, + "frontend/src/lib/components/translate/ConfigTabForm.svelte": 10509298384333710233, + "frontend/build/_app/immutable/chunks/C2FqdT6I.js": 8686021140020127525, + "venv/lib/python3.13/site-packages/git/index/fun.py": 18338379518768926372, + "docs/api/html/group__test__list__jobs__with__status__filter.html": 5736540040640427487, + "venv/lib/python3.13/site-packages/cffi/parse_c_type.h": 9889309594462820068, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286.pyf": 14358606965464506398, + "venv/lib/python3.13/site-packages/anyio/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/lexers/rebol.py": 10460732731157940232, + "frontend/src/components/git/CommitModal.svelte": 12274093894389435426, + "docs/api/html/group__MaintenanceDashboardStateStatus.html": 16864192271527139550, + "docs/api/html/group__SupersetDatasetsPreviewFiltersMixin.html": 6453705945278224985, + "venv/lib/python3.13/site-packages/pandas/core/util/__init__.py": 15130871412783076140, + "docs/api/html/group__derive__recommended__action.html": 17143048057272072667, + "docs/api/html/group__report__service.html": 3161527402315470112, + "venv/lib/python3.13/site-packages/pygments/lexers/_ada_builtins.py": 16331776886501464264, + "venv/lib/python3.13/site-packages/pydantic/experimental/arguments_schema.py": 8644805798814234457, + "dist/docker/ss-tools-backend.0.1.4.tar.xz": 13469142969357194454, + "docs/api/html/group__test__execution__snapshot__includes__recovered__imported__filters__without__template__mapping.html": 4769785789675369801, + "venv/lib/python3.13/site-packages/pygments/lexers/pascal.py": 5992422120590352714, + "venv/lib/python3.13/site-packages/pygments/formatters/terminal.py": 14380579279194052030, + "venv/lib/python3.13/site-packages/referencing/tests/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/packaging/version.py": 13470993734108007779, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__load__excel__mappings.html": 1764004230841108037, + "venv/lib/python3.13/site-packages/psycopg2/extensions.py": 14197445323992851566, + "venv/lib/python3.13/site-packages/PIL/CurImagePlugin.py": 17697303205012181529, + "docs/api/html/group__test__get__dashboards__invalid__pagination.html": 5025034056693178117, + "docs/api/html/group__SmokeE2E.html": 5059040919589105636, + "docs/api/html/group__MarkerLogger__explore.html": 6168160399723675032, + "venv/lib/python3.13/site-packages/git/util.py": 7424113599506269603, + "dist/docker/ss-tools-backend.0.1.8.tar.xz": 1920838464713673524, + "docs/api/html/group__ProfileType.html": 7571730309739466085, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/_elffile.py": 5728213398707817049, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_to_series.py": 10922287546912569276, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_sort.py": 11941630642630249576, + "docs/api/html/group__deleteFile__Function.html": 12741403507383620880, + "venv/lib/python3.13/site-packages/numpy/polynomial/__init__.pyi": 8708830448398500675, + "venv/lib/python3.13/site-packages/PIL/ExifTags.py": 6869879826623916830, + "docs/api/html/group__test__migrate__dashboards__no__ids.html": 5314005886091290809, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py": 1331438198531942242, + "venv/lib/python3.13/site-packages/pygments/styles/autumn.py": 8750937312198920971, + "docs/api/html/group__init__repository.html": 16356863681930678181, + "docs/api/html/group__TestOrchestratorInsertFlow.html": 10014979661308261378, + "venv/lib/python3.13/site-packages/httpx/_transports/wsgi.py": 11981127094916412794, + "docs/api/html/group__TranslationPlanner.html": 11482656897357674989, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/mixed/foo_free.f90": 8821492401453445757, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____writeTabToUrl.html": 13401008738381827718, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_concat.py": 16549010224568175202, + "frontend/src/routes/dashboards/[id]/components/DashboardGitManager.svelte": 8067644174227406298, + "venv/lib/python3.13/site-packages/pandas/tests/io/sas/test_byteswap.py": 5090287710338893350, + "docs/api/html/group__background__tasks.html": 7187534320763437047, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/text.py": 6096215585499446070, + "venv/lib/python3.13/site-packages/pip/_vendor/packaging/metadata.py": 7204343250279221076, + "docs/api/html/group__resetForm__Function.html": 7796618286277147047, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/interval/test_interval_new.py": 3893421355544517336, + "docs/api/html/group__handleBulkPush__Function.html": 5303517562095146306, + "venv/lib/python3.13/site-packages/pygments/lexers/ezhil.py": 10294520768762308887, + "venv/lib/python3.13/site-packages/pygments/lexers/julia.py": 13516407944275342328, + "venv/lib/python3.13/site-packages/numpy-2.4.2.dist-info/licenses/numpy/_core/src/umath/svml/LICENSE": 9584102918819171104, + "venv/lib/python3.13/site-packages/numpy/_core/_asarray.py": 3468915833437229425, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/ddl.py": 5006132143435653867, + "venv/lib/python3.13/site-packages/referencing/tests/test_core.py": 8280983300775878144, + "frontend/src/lib/stores/__tests__/test_taskDrawer.js": 14849615218402542643, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/array_from_pyobj/wrapmodule.c": 17333399794446198412, + "frontend/src/routes/validation/history/+page.svelte": 17086781413900596714, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/test_period_range.py": 9598872514187524706, + "docs/api/html/group__DatasourceColumnsResponse.html": 1250225195653267212, + "docs/api/html/group__TranslateRunsApi.html": 11590382264152453045, + "backend/git_repos/remote/test-repo.git/hooks/update.sample": 8204581436750313444, + "docs/api/html/group____get__orchestrator.html": 13890254604779025703, + "docs/api/html/group____cleanup__history__ttl.html": 12209285080355144481, + "docs/api/html/group__derive__final__status.html": 4319149296078719364, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/latex_longtable.tpl": 12723621983797415444, + "docs/api/html/group__llm__prompt__templates.html": 11148828659101585687, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/common/block.f": 10727418304254548946, + "frontend/src/lib/components/translate/__tests__/test_bulk_replace_modal.svelte.js": 16164320505605430516, + "venv/lib/python3.13/site-packages/PIL/ImageDraw.py": 12919304671639673744, + "docs/api/html/group__test__maintenance__api.html": 10206796814843197798, + "docs/api/html/group__frontend__src__services__storageService__js____deleteFile.html": 7628993621903207351, + "venv/lib/python3.13/site-packages/pygments/formatters/rtf.py": 5850574956253747036, + "docs/api/html/group__MaintenanceSettingsResponse.html": 15485267918904014972, + "docs/api/html/group__type__profiles.html": 8266323522750752760, + "docs/api/html/group__TestCleanReleaseTui.html": 12992766318749624082, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/starlette/middleware/__init__.py": 9524277524025372856, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/padding.py": 7470483648602195268, + "docs/api/html/group__normalize__start__page.html": 845187168959113829, + "venv/lib/python3.13/site-packages/pandas/tests/interchange/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/cryptography/x509/verification.py": 1079694784860492682, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_map.py": 10769974470610467636, + "venv/bin/numpy-config": 17104619511899110252, + "backend/src/models/dataset_review_pkg/_finding_models.py": 17866785133899667582, + "backend/src/services/health_service.py": 7707107506464919176, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/constant_time.py": 2499026008403328245, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/__init__.py": 913610541217778747, + "frontend/src/lib/components/dataset-review/__tests__/us3_execution_batch.ux.test.js": 17821671753127603158, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_series.py": 17126297747681689408, + "docs/api/html/group____update__semantic__field__state.html": 9761433395553999447, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_invalid.py": 2762344062554575805, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/plugin/bootstrap.py": 3865947239497500815, + "venv/lib/python3.13/site-packages/keyring/devpi_client.py": 3445890176686211982, + "docs/api/html/group__reload__validation__policy.html": 6168739793611833472, + "venv/lib/python3.13/site-packages/pandas/core/sorting.py": 689057022713741051, + "docs/api/html/group__DictionaryImportResult.html": 10670594679605704236, + "venv/lib/python3.13/site-packages/pygments/lexers/javascript.py": 16869490851285457857, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libcrypto-2e26a48f.so.3": 3658133521245392852, "venv/lib/python3.13/site-packages/numpy/_core/records.pyi": 14194354215405544730, - "docs/api/html/group__test__delete__tasks__empty__list.html": 17672043131209530159, - "venv/lib/python3.13/site-packages/pydantic/v1/types.py": 10761779263335288559, - "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/return_logical/foo90.f90": 12508663906609241323, - "docs/api/html/group__frontend__src__lib__auth__store__ts____createAuthStore.html": 4553975624933631744 + "frontend/build/_app/immutable/chunks/CvogWEa-.js": 8321615869236355654, + "docs/api/html/group__ApiRoutesModule.html": 7565963372446460595, + "venv/lib/python3.13/site-packages/authlib/oidc/discovery/models.py": 6924198551176217533, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/ufunc_config.pyi": 2005481648919369399, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parsing.py": 2228959015420278458, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setSession.html": 5227957195092266456, + "docs/api/html/group__save__recovery__state.html": 16571398624709231386, + "docs/api/html/group__AssistantClarificationIntegrationTest__Module.html": 8857034340039410277, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_reindex.py": 12459492712890114705, + "docs/api/html/group__GitRepository.html": 11611249034372487005, + "venv/lib/python3.13/site-packages/passlib/handlers/bcrypt.py": 8125664010268654055, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_compat.py": 6032546839876195115, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/typst.tpl": 5055419073126793137, + "frontend/build/_app/immutable/chunks/BmmG8642.js": 9237554532821302070, + "venv/lib/python3.13/site-packages/pandas/core/arrays/interval.py": 16281221279220043867, + "docs/api/html/search/all_a.js": 3849984850211567813, + "docs/api/html/group__buildReportQueryString__Function.html": 10840805796711813208, + "frontend/build/_app/immutable/nodes/26.CBUNqgm5.js": 10891710782968022841, + "backend/src/plugins/translate/__tests__/test_dictionary_crud.py": 9330652377352390414, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/lib_utils.pyi": 12312677132342982866, + "backend/src/plugins/translate/events.py": 16974204038587054475, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_block_docstring.py": 18146459246837890489, + "backend/tests/test_datasets.py": 561733205241314734, + "venv/lib/python3.13/site-packages/numpy/_pyinstaller/__init__.pyi": 15130871412783076140, + "docs/api/html/group__DatasetReviewSessionStoreTests__Module.html": 5623361748252231964, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x25519.pyi": 11455144434558323236, + "venv/lib/python3.13/site-packages/uvicorn/lifespan/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_common.py": 9446944526144164255, + "docs/api/html/group__providers.html": 16950412038675555694, + "docs/api/html/group__get__all__plugin__configs.html": 14923346516628235508, + "docs/api/html/group__delete__schedule.html": 109693276017801304, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/sync_openid.py": 5670082466989878756, + "frontend/src/lib/components/ui/MultiSelect.svelte": 1475503303018683038, + "docs/api/html/group__test__deprecation__comment__exists.html": 5453492904479567908, + "docs/api/html/group__build__initial__profile.html": 17311912746267862536, + "venv/lib/python3.13/site-packages/numpy/f2py/src/fortranobject.h": 5980733638838895780, + "docs/api/html/group__CheckFinalStatus.html": 8660527585364518838, + "docs/api/html/group__AnswerKind.html": 4721759660402979766, + "docs/api/html/group__insert__batch__to__target.html": 389711692439722572, + "docs/api/html/group__DatasetReviewApi__Module.html": 3012825213898058146, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/RECORD": 4487477737768669516, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/parameter/constant_non_compound.f90": 8048650253889213460, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/test_converter.py": 7709581879446228856, + "venv/lib/python3.13/site-packages/sqlalchemy/pool/events.py": 13473858896579666058, + "venv/lib/python3.13/site-packages/urllib3/contrib/socks.py": 645086578806871685, + "venv/lib/python3.13/site-packages/pygments/lexers/parasail.py": 7758704230750989282, + "docs/api/html/group____validate__bcp47.html": 12782097460405837198, + "frontend/src/lib/stores/__tests__/sidebar.test.js": 3057168376995255911, + "docs/api/html/group__test__transform__yaml__nonexistent__file.html": 12815312108870320072, + "venv/lib/python3.13/site-packages/bcrypt/_bcrypt.abi3.so": 8691731321358529264, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_series_apply.py": 12349190935180702377, + "venv/lib/python3.13/site-packages/numpy/linalg/_linalg.pyi": 9629457630163613604, + "docs/api/html/group__test__get__repository__status__batch__marks__item__as__error__on__service__failure.html": 15089064352958594879, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/JaroWinkler_py.py": 16189204441438239231, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/licenses/LICENSE": 4123064337800029995, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numerictypes.pyi": 15443101937711720707, + "frontend/src/routes/admin/settings/+page.svelte": 9726389419291634649, + "docs/api/html/group__suggest__mappings__api.html": 3498879923583655660, + "venv/lib/python3.13/site-packages/starlette/_exception_handler.py": 9892172656847200124, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_lexsort.py": 10394832796613968484, + "venv/lib/python3.13/site-packages/passlib/utils/des.py": 2650106381178222229, + "venv/lib/python3.13/site-packages/passlib/handlers/__init__.py": 3424087230285514388, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/types.py": 1343652083039949282, + "venv/lib/python3.13/site-packages/pandas/tests/extension/test_categorical.py": 1329115818264527103, + "docs/api/html/group__DashboardHub__loadDashboards__Function.html": 3241502516261310568, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_datetime.py": 11417836378782978454, + "venv/lib/python3.13/site-packages/pandas/core/groupby/ops.py": 2036968385544457016, + "docs/api/html/group____build__profile__preference__stub.html": 17148946407396504332, + "docs/api/html/group__map__columns.html": 11522895411125783141, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9101/registration.py": 14639972512303032712, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/misc.py": 6571241778569465264, + "venv/lib/python3.13/site-packages/sqlalchemy/cyextension/collections.pyx": 17138729833735744777, + "docs/api/html/group__MaintenanceDashboardScanner.html": 7477142727721457580, + "venv/lib/python3.13/site-packages/pandas/tests/series/test_unary.py": 8046543893978198672, + "docs/api/html/group__deploy__dashboard.html": 874940735926289549, + "venv/lib/python3.13/site-packages/keyring/backends/chainer.py": 12526770797516553385, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/kbkdf.py": 1359143503575245224, + "docs/api/html/group____prepare__owned__session__mutation.html": 12698201251511839350, + "venv/lib/python3.13/site-packages/jeepney-0.9.0.dist-info/licenses/LICENSE": 11986475601091748012, + "venv/lib/python3.13/site-packages/pygments/lexers/q.py": 15426466074881683794, + "docs/api/html/group__generate__commit__message.html": 252727964148470828, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth1/resource_protector.py": 17080006010679257864, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/mod.py": 2054876501774773382, + "frontend/src/lib/components/translate/TranslationRunProgress.svelte": 12659537213220151902, + "docs/api/html/group__frontend__src__types__dashboard__ts____DashboardMetadata.html": 16596283258366470252, + "venv/lib/python3.13/site-packages/pyasn1/type/univ.py": 7163437624197683039, + "docs/api/html/group__test__reports__list__openapi__required__keys.html": 16840541688018374528, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/RECORD": 6349071557579241461, + "venv/lib/python3.13/site-packages/pandas/core/methods/selectn.py": 13552176539021625776, + "docs/api/html/group__TestLogPersistence.html": 3552045492355387515, + "venv/lib/python3.13/site-packages/pandas/tests/generic/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_extension.py": 4831602527247293866, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/_internal_utils.py": 4091305333167213279, + "docs/api/html/group__CleanReleaseTUI.html": 9540635457913551614, + "backend/src/core/middleware/__init__.py": 13917394130915533209, + "docs/api/html/group____resolve__task__name.html": 2876621474958502481, + "venv/lib/python3.13/site-packages/authlib/integrations/requests_client/oauth2_session.py": 7076806577412682835, + "docs/api/html/group__fetchDashboards__Function.html": 15265666455140275558, + "docs/api/html/group__test__get__reports__default__pagination__contract.html": 2422991810127559996, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/hmac.py": 18132740541139891824, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh24662.f90": 4241310200087270990, + "docs/api/html/group__MapperPluginModule.html": 12912874537129750677, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/__init__.py": 2345997696115514124, + "docs/api/html/group__GitPlugin.html": 16223539126912136426, + "docs/api/html/group__test__get__payload__preserves__legacy__sections.html": 10719382727830491309, + "venv/lib/python3.13/site-packages/PIL/IptcImagePlugin.py": 14329453455296600824, + "docs/api/html/group__handleFilterChange__Function.html": 12314374793481426477, + "venv/lib/python3.13/site-packages/starlette/middleware/errors.py": 1337979724602864440, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23598.f90": 1409455010615482779, + "docs/api/html/group__ClarificationQuestion.html": 5390726230912714507, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/privatemod.f90": 8999550841368314739, + "docs/api/html/group__ReportsApi.html": 8927820650969319266, + "docs/api/html/group__reflect.html": 5691625797786994210, + "docs/api/html/group__DEFAULT__LLM__ASSISTANT__SETTINGS.html": 18119385185452388205, + "docs/api/html/dir_bff1a75f97c7ace650ee0c8d57d0aaa5.js": 14348227999396735008, + "docs/api/html/navtreeindex12.js": 15950560879981123236, + "docs/api/html/group__test__sync__network__404__mapping__keeps__non__dashboard__endpoints__generic.html": 12571154995084509267, + "docs/api/html/group__test__existing__test__would__fail.html": 6044563429962222801, + "backend/src/core/task_manager/__init__.py": 15991802770215124472, + "docs/api/html/search/all_b.js": 1386014000926361523, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_aggregate.py": 15038915958422670700, + "venv/lib/python3.13/site-packages/numpy/_core/memmap.py": 4093510602708416858, + "docs/api/html/group__ValidationTaskListResponse.html": 14158939387498447501, + "docs/api/html/group__TermCorrectionPopup.html": 7641550414720902162, + "venv/lib/python3.13/site-packages/pandas/_libs/window/__init__.py": 15130871412783076140, + "docs/api/html/group__RootLayoutConfig__Module.html": 11134316622827325111, + "docs/api/html/group__TranslationStageRunner.html": 5118717378303155773, + "docs/api/html/group__test__get__datasets__success.html": 17085020636702913343, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_kurt.py": 2898715045429898166, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_na_indexing.py": 8712547126522523899, + "venv/lib/python3.13/site-packages/pandas/tests/extension/decimal/test_decimal.py": 17811784255174057561, + "docs/api/html/group____run__task.html": 10393447256976591681, + "venv/lib/python3.13/site-packages/pandas/io/formats/__init__.py": 18140261896107421679, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/dml.py": 5340326603935367534, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_join.py": 17508073052750406350, + "docs/api/html/group____latest__decision__for__candidate.html": 17368372638299477528, + "docs/api/html/group__MappingService.html": 14790116697814336524, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____stringifyValue.html": 757351777204500206, + "docs/api/html/group__TestArchiveParser.html": 15611159576834520636, + "docs/api/html/group__SettingsE2E.html": 1243339859806566484, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_comparisons.py": 2764694394636775220, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7636/challenge.py": 6388445104761482746, + "docs/api/html/group__DatasetReviewSessionStore.html": 16665757289211685601, + "docs/api/html/group__run__revoke.html": 8815559895593475894, + "venv/lib/python3.13/site-packages/ecdsa/ellipticcurve.py": 15624510849346629163, + "venv/lib/python3.13/site-packages/pygments/lexers/foxpro.py": 14032656072384708293, + "venv/lib/python3.13/site-packages/pandas/core/flags.py": 4690844374637430013, + "docs/api/html/group__ClarificationAnswerCommand.html": 1340014238187838879, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/gh23879.f90": 13174951794239860144, + "venv/lib/python3.13/site-packages/PIL/FtexImagePlugin.py": 8196606399875185931, + "venv/lib/python3.13/site-packages/PIL/ImageWin.py": 13635870584232081323, + "frontend/src/routes/storage/repos/+page.svelte": 2273592022555593164, + "venv/lib/python3.13/site-packages/click-8.3.1.dist-info/RECORD": 3649834117451110628, + "docs/api/html/group__profile__route__deps__fixture.html": 16691986014866091416, + "venv/lib/python3.13/site-packages/anyio/_core/_streams.py": 6436461519284884601, + "venv/lib/python3.13/site-packages/pydantic/color.py": 1007422551673129827, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/test_index.py": 10837492069089787028, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_put.py": 8547660958963625817, + "venv/lib/python3.13/site-packages/sqlalchemy/inspection.py": 8563914460959280101, + "venv/lib/python3.13/site-packages/pip/_vendor/pkg_resources/__init__.py": 1677532855362260338, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_set_index.py": 10971093361190283675, + "frontend/src/routes/+error.svelte": 9437886843842311915, + "docs/api/html/group____resolve__candidate__source__version.html": 16340094199420833679, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/incfile.f90": 4348588746493647387, + "docs/api/html/group__LoggingConfig.html": 5718768754954658081, + "docs/api/html/group__await__input.html": 16300840224339823745, + "docs/api/html/group____resolve__environment__id.html": 18224093667095318191, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/licenses/LICENSE": 3509878235770224233, + "docs/api/html/group__test__null__source__data__handled.html": 9321216567493581904, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/metadata.py": 1362166613223529108, + "frontend/src/routes/validation/[id]/+page.svelte": 16757987409433040910, + "docs/api/html/group__get__dialect__from__database.html": 988662418075902152, + "docs/api/html/group____collect__user__permission__pairs.html": 10134810142124377145, + "docs/api/html/search/groups_9.js": 11508513771310591301, + "docs/api/html/group__fetch__datasource__metadata.html": 12758308588265556383, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/negative_bounds/issue_20853.f90": 3911644649277659333, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_common.py": 3559416690270529904, + "docs/api/html/group____strip__html__tags.html": 1085064762657441380, + "venv/lib/python3.13/site-packages/pip/_internal/utils/filesystem.py": 7716285033598020316, + "venv/lib/python3.13/site-packages/numpy/polynomial/legendre.py": 1731362089136926666, + "docs/api/html/group__GitServiceRemoteMixin.html": 16511670561438840755, + "docs/api/html/group__test__update__session__surfaces__commit__time__session__version__conflict__payload.html": 11776750589639859419, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/padding.py": 11244071766694094554, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/lib_version.py": 10384171076976071398, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/progress_bar.py": 15595912766961891283, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_defchararray.py": 15135904718007443306, + "venv/lib/python3.13/site-packages/pandas/tests/arithmetic/test_string.py": 15542851215119662814, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/methods/__init__.py": 15130871412783076140, + "frontend/src/routes/+layout.ts": 8392799000856778740, + "docs/api/html/group__BackupTypes.html": 10757767469167289047, + "venv/lib/python3.13/site-packages/anyio/_core/_synchronization.py": 7050001375718876608, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/errors.py": 14634768861119075506, + "venv/lib/python3.13/site-packages/_pytest/mark/expression.py": 1821437025841201912, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_pickle.py": 388513670243918273, + "venv/lib/python3.13/site-packages/git/diff.py": 6691274015605451565, + "docs/api/html/group____load__db__mapping.html": 5085581099428529195, + "docs/api/html/group__sanitize__secret.html": 18277308754712559382, + "frontend/src/lib/stores/__tests__/assistantChat.test.js": 11108218971449015168, + "docs/api/html/group__frontend__src__services__adminService__js____getRoles.html": 5558475973207051005, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_equals.py": 9405821443172167066, + "docs/api/html/search/groups_16.js": 8625558773447853349, + "backend/git_repos/remote/test-repo.git/objects/38/0395343915d386a8d58774cc6c6677205065e8": 5481382229802988191, + "docs/api/html/group__get__schema.html": 13895060400411219452, + "docs/api/html/group__AuthJwtModule.html": 11179759053901968829, + "docs/api/html/group__LoggingSettings.html": 1027847486912759228, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/string/gh25286_bc.pyf": 10893958728966010450, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_argparse.py": 11111699418217357084, + "docs/api/html/group__description.html": 1734080948702838188, + "docs/api/html/group__save__and__unpack__dashboard.html": 8946899899744210343, + "venv/lib/python3.13/site-packages/keyring/backends/null.py": 12740996416911604870, + "docs/api/html/group__RoutesTestsConftest.html": 1558041571936568459, + "backend/conftest.py": 16521922883434363388, + "backend/src/api/routes/assistant/_tool_deploy.py": 7694782162857068333, + "venv/lib/python3.13/site-packages/numpy/f2py/use_rules.pyi": 18395688193449688360, + "docs/api/html/group____build__no__repo__status__payload.html": 15081425468629205529, + "venv/lib/python3.13/site-packages/gitdb/typ.py": 16374739906244805047, + "docs/api/html/group__update__my__preference.html": 13895164481872930227, + "docs/api/html/group__test__test__git__config__validates__connection__successfully.html": 7885701889149733525, + "docs/api/html/group__DatasetReviewSchemaComposites.html": 187782560876687868, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/horizontal_shard.py": 17615732108308858890, + "docs/api/html/group__SupersetAuthCache.html": 17439611318511412216, + "docs/api/html/group__get__git__configs.html": 3034817285370598287, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli/py.typed": 9796674040111366709, + "frontend/src/lib/components/health/PolicyForm.svelte": 17995152707951193436, + "backend/src/models/dataset_review_pkg/_semantic_models.py": 17385189153412898824, + "venv/lib/python3.13/site-packages/pandas/core/window/__init__.py": 15180240812077877933, + "venv/lib/python3.13/site-packages/greenlet/TThreadStateCreator.hpp": 9887867766190459665, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_fillna.py": 8802093795040069423, + "frontend/src/lib/stores/activity.js": 10599836924066718828, + "venv/lib/python3.13/site-packages/jaraco_context-6.0.2.dist-info/METADATA": 14180973198574731599, + "docs/api/html/group__SupersetDatasetsPreviewMixin.html": 9433049209236361670, + "docs/api/html/navtreeindex7.js": 3618174960812941697, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/jeepney/io/tests/conftest.py": 13381752264278264218, + "docs/api/html/group____wait__for__terminal__task.html": 10847232788702296305, + "docs/api/html/group__test__auth__config__module__contract.html": 10228088760530679113, + "docs/api/html/group__HomePage.html": 10705890495789315886, + "docs/api/html/group__updateTask__Function.html": 11102333396610846970, + "docs/api/html/group__sync__environment.html": 1811187775036019320, + "docs/api/html/group__MergeConflictFileSchema.html": 6451830131202489447, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_scalarbuffer.py": 16859789411010045795, + "backend/src/plugins/translate/dictionary_validation.py": 16674754716565921261, + "docs/api/html/tab_b.png": 17139676106204569061, + "docs/api/html/group__handleSave__Function.html": 12255691954229374900, + "docs/api/html/group__test__create__task__missing__fields.html": 14663889192866601882, + "venv/lib/python3.13/site-packages/pandas/tests/test_common.py": 8987965560487646705, + "venv/lib/python3.13/site-packages/numpy/fft/_helper.py": 15906444100774706395, + "venv/lib/python3.13/site-packages/sqlalchemy/util/preloaded.py": 3719702436309355562, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_assert_categorical_equal.py": 7942323667866280872, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/transform/test_numba.py": 8250377774229977284, + "docs/api/html/group__MIN__MAX__TOKENS.html": 3548157067782585566, + "docs/api/html/group__UseReviewSession.html": 15548286754237500808, + "venv/lib/python3.13/site-packages/pygments/lexers/_mysql_builtins.py": 16087227298679432006, + "venv/lib/python3.13/site-packages/requests/exceptions.py": 6474239743209354989, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_style.py": 14810883299259169406, + "docs/api/html/group__manifest__consistency.html": 302557474349744915, + "venv/lib/python3.13/site-packages/requests/hooks.py": 9888227370911151341, + "venv/lib/python3.13/site-packages/uvicorn/middleware/wsgi.py": 9179913252056394958, + "venv/lib/python3.13/site-packages/pygments/lexers/asc.py": 10744437946030875496, + "frontend/src/lib/components/health/HealthMatrix.svelte": 18141816318382901531, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/_numpyconfig.h": 18150055639397773687, + "frontend/build/_app/immutable/nodes/4.D9CxVeqh.js": 9906436066539068852, + "venv/lib/python3.13/site-packages/starlette/responses.py": 14081178833432955042, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_client/apps.py": 10758062668702044519, + "venv/lib/python3.13/site-packages/charset_normalizer/constant.py": 3070316197495651997, + "docs/api/html/group__test__get__datasets__pagination__zero__page.html": 13580035186306666421, + "docs/api/html/group__DashboardHub__handleRestoreProfileFilter__Function.html": 13810703798808089221, + "venv/lib/python3.13/site-packages/pip/_internal/cli/cmdoptions.py": 12107733000019393631, + "docs/api/html/open.png": 3773965092868909301, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____listDashboardBanners.html": 6341650642747001563, + "docs/api/html/group__selectedPlugin__Data.html": 6186646738464235130, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/INSTALLER": 17282701611721059870, + "backend/src/api/routes/assistant/_command_parser.py": 17528436899276199792, + "docs/api/html/group__test__get__dashboards__page__context__other__disables__profile__default.html": 1375161324945488509, + "docs/api/html/group__ReleaseCandidateStatus.html": 5853358063003918750, + "docs/api/html/group__ReportsApiTest__Module.html": 14793467354050182333, + "frontend/src/components/EnvSelector.svelte": 593737239668879382, + "backend/src/services/clean_release/__tests__/test_stages.py": 15550789367780507193, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth1/__init__.py": 14481526616887213120, + "docs/api/html/group__SupersetProfileLookup.html": 7471338261277310068, + "frontend/src/lib/components/reports/__tests__/report_detail.ux.test.js": 7841254655898186872, + "docs/api/html/group____resolve__dashboard__id__by__ref.html": 16216818858438055057, + "docs/api/html/dir_49e56c817e5e54854c35e136979f97ca.html": 1429166617003811202, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/test_frame_subplots.py": 4596089143375088346, + "venv/lib/python3.13/site-packages/itsdangerous-2.2.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic/v1/datetime_parse.py": 10467114625921924095, + "venv/lib/python3.13/site-packages/python_dotenv-1.2.1.dist-info/METADATA": 2604612822968175379, + "docs/api/html/group__resetEnvForm__Function.html": 11629856738911116007, + "docs/api/html/group__get__tasks.html": 6039993422624738994, + "docs/api/html/group__test__list__gitea__repositories__returns__payload.html": 8122355547255221781, + "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_generation_shared.py": 23345689300073549, + "docs/api/html/group__build__security__summary.html": 9447535612374879505, + "docs/api/html/group__frontend__src__lib__stores__sidebar__js____closeMobile.html": 6283987850786715384, + "docs/api/html/group__test__user__role__association.html": 6316579439480183046, + "frontend/src/lib/components/reports/__tests__/reports_list.ux.test.js": 10301023261856534116, + "docs/api/html/group__get__sources.html": 16601137258119600037, + "venv/lib/python3.13/site-packages/pygments-2.19.2.dist-info/licenses/AUTHORS": 2987771679926243782, + "frontend/src/routes/settings/StorageSettings.svelte": 1953157077500445570, + "docs/api/html/globals_h.html": 17479549949189588951, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_swaplevel.py": 3584770792383216626, + "frontend/src/lib/components/reports/ReportsList.svelte": 17367781544989044334, + "docs/api/html/group____run__async.html": 1312288416173170323, + "docs/api/html/group__update__preview__row.html": 12779817647568588582, + "docs/api/html/group__TestConstantsAuditFixes.html": 1281986242324537213, + "venv/lib/python3.13/site-packages/passlib-1.7.4.dist-info/WHEEL": 9131187629260560775, + "docs/api/html/group__generate__api__key.html": 2424917664725615867, + "docs/api/html/group__list__sessions.html": 4853566056432919079, + "docs/api/html/group____RecordingNetworkClient.html": 10311785607548179860, + "venv/lib/python3.13/site-packages/_pytest/timing.py": 14080119171718125986, + "venv/lib/python3.13/site-packages/_pytest/reports.py": 13930709338396699351, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/bitwise_ops.pyi": 15598239133239520456, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/numeric/test_astype.py": 4854809596041987380, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_value_counts.py": 9352540710376306520, + "docs/api/html/group__BulkCorrectionSidebar.html": 5126405434557743105, + "venv/lib/python3.13/site-packages/python_jose-3.5.0.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/gitdb/pack.py": 7828244200815444166, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas_constants.py": 14492427939536692313, + "frontend/src/components/tasks/TaskLogPanel.svelte": 10382066588776421689, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc9068/token_validator.py": 2932866595454490527, + "venv/lib/python3.13/site-packages/cffi/_embedding.h": 9441285871823018603, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_indexing.py": 6539365174681852769, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/numeric.py": 14451517189908259173, + "docs/api/html/group__flush__task__logs.html": 6410178404460258558, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/default_styles.py": 4469204219504675080, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/rsa.pyi": 13850254283540522444, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/toml.py": 2225382735669153136, + "venv/lib/python3.13/site-packages/pydantic/_internal/_docs_extraction.py": 5210114991139191634, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_format.py": 5231304674283854194, + "frontend/build/_app/immutable/chunks/B5fvJR8I.js": 3279841246384223130, + "backend/src/api/routes/tasks.py": 13688847871690186483, + "docs/api/html/group____build__api__url.html": 12259647430202722901, + "venv/lib/python3.13/site-packages/pandas/tests/frame/test_logical_ops.py": 16166386974005224415, + "venv/lib/python3.13/site-packages/numpy/dtypes.py": 14128421398870718692, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_arithmetic.py": 13304014482504911016, + "venv/lib/python3.13/site-packages/h11/_readers.py": 832030916375670301, + "venv/lib/python3.13/site-packages/pandas/tests/reshape/concat/test_empty.py": 13006241382741353569, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_deprecated_kwargs.py": 10379929935087904974, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/LICENSE": 6774760218010069963, + "venv/lib/python3.13/site-packages/PIL/_avif.cpython-313-x86_64-linux-gnu.so": 5243449968857258659, + "backend/src/plugins/llm_analysis/__tests__/test_service.py": 17317408415318686082, + "venv/lib/python3.13/site-packages/numpy/lib/introspect.py": 5706729538356302335, + "docs/api/html/group__FilterRecoveryStatus.html": 15412053149747747307, + "venv/lib/python3.13/site-packages/pandas/io/xml.py": 326466573926392764, + "docs/api/html/group__test__execution__error__handled.html": 16361858862762216227, + "venv/lib/python3.13/site-packages/click/_utils.py": 16491408631876616271, + "venv/lib/python3.13/site-packages/jaraco/classes/ancestry.py": 11742194518360056470, + "docs/api/html/group__test__patch__dashboard__metadata__handles__missing__targets.html": 6681139266574011125, + "venv/lib/python3.13/site-packages/_pytest/_py/error.py": 2257336434271433543, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/data_with_comments.f": 3980077223827767077, + "docs/api/html/group__migrate__dashboards.html": 825298586869034371, + "venv/lib/python3.13/site-packages/PIL/ImageText.py": 15322496455608242001, + "venv/lib/python3.13/site-packages/pydantic/deprecated/class_validators.py": 2200511838255373813, + "venv/lib/python3.13/site-packages/pygments/styles/igor.py": 2291640843781128270, + "venv/lib/python3.13/site-packages/pygments/lexers/rdf.py": 14488319292387226505, + "docs/api/html/group__test__extract__native__filters__from__permalink.html": 12643899760315874197, + "docs/api/html/group__remove__validation__job.html": 18264277450501764734, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/npy_no_deprecated_api.h": 7896028550808252043, + "docs/api/html/group__DashboardHub__handleSort__Function.html": 13034575267790584101, + "docs/api/html/group__TestConfigManagerDefaults.html": 5818758037140085192, + "venv/lib/python3.13/site-packages/ecdsa-0.19.1.dist-info/RECORD": 10552906886624188222, + "docs/api/html/group__SeedPermissionsScript.html": 3283052414462232547, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/tornado.py": 6491136785917169991, + "frontend/playwright-report/trace/index.html": 6654608714409350530, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/repr.py": 839417422880281122, + "venv/lib/python3.13/site-packages/apscheduler/triggers/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/accesstype.f90": 877982462186396830, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_formats.py": 3525283725314253763, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_to_dict.py": 16252265723192124609, + "frontend/src/routes/datasets/__tests__/StatsBar.test.js": 16218511958634350787, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/frequencies/__init__.py": 15130871412783076140, + "docs/api/html/group__InlineEditRequest.html": 11326341888929414464, + "docs/api/html/group__test__repository__rejects__report__overwrite__for__same__report__id.html": 12621310059402333179, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/test_xml.py": 6906547816798138000, + "docs/api/html/group__ApiKeyCreateResponse.html": 9522527415314064226, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_ratio.py": 940213488194452623, + "backend/tests/scripts/test_clean_release_tui_v2.py": 5872535102885848729, + "docs/api/html/group__TestClickHouseInsertSqlGeneration.html": 10830522054938082095, + "venv/lib/python3.13/site-packages/attr/exceptions.pyi": 12323493441583681402, + "docs/api/html/group__ComplianceStageBase.html": 5577816396402425153, + "venv/lib/python3.13/site-packages/_pytest/config/findpaths.py": 7867603756389940562, + "venv/lib/python3.13/site-packages/numpy/lib/_utils_impl.py": 13012775703413938576, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_base_indexer.py": 3129123098288498633, + "docs/api/html/group__VariableKind.html": 14364200722064448242, + "docs/api/html/group__RetentionPolicy.html": 12974101386887046578, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_equals.py": 11041884432990762650, + "venv/lib/python3.13/site-packages/git/cmd.py": 8854786163152637148, + "venv/lib/python3.13/site-packages/pandas/core/array_algos/datetimelike_accumulations.py": 14719894187829729175, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/usecols/test_parse_dates.py": 5344929031479212951, + "frontend/src/lib/components/translate/TranslationMetricsDashboard.svelte": 8577281635452165836, + "docs/api/html/group__PreviewRowUpdate.html": 17634414564160299335, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/fail/nested_sequence.pyi": 10985533017688918283, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/keys.pyi": 1846673463183660140, + "venv/lib/python3.13/site-packages/pandas/tests/apply/test_invalid_arg.py": 610891871484326055, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/test_setops.py": 15196593522537569500, + "venv/lib/python3.13/site-packages/pygments/lexers/fantom.py": 1986459352773446836, + "docs/api/html/group__get__dashboard__async.html": 462527770103196028, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_setops.py": 15365064654930699428, + "docs/api/html/group__storageService__Module.html": 12420552828308744419, + "docs/api/html/group__test__sync__environment__requires__existing__env.html": 7580953010735906417, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/kdf/concatkdf.py": 6712732248328458871, + "venv/lib/python3.13/site-packages/requests/_internal_utils.py": 4091305333167213279, + "venv/lib/python3.13/site-packages/pandas/core/groupby/generic.py": 4857114071347950833, + "docs/api/html/group__frontend__src__services__adminService__js____getADGroupMappings.html": 9312026240413982380, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_to_csv.py": 6678500392137188521, + "docs/api/html/group__SidebarTest.html": 12608676498908713689, + "docs/api/html/group__AssistantMessageResponse.html": 16366901716381602745, + "venv/lib/python3.13/site-packages/pip/_internal/utils/wheel.py": 450504439085405418, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/crackfortran/publicmod.f90": 2174121054736811903, + "frontend/src/routes/datasets/review/+page.svelte": 8512810578824138490, + "docs/api/html/group__GitServiceStatusMixin.html": 4523615132638465854, + "frontend/src/routes/datasets/review/ReviewWorkspaceLeftSidebar.svelte": 9009994851177005358, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/test_concat.py": 3788461606817482165, + "backend/src/api/routes/translate/_run_routes.py": 16216908966766661647, + "venv/lib/python3.13/site-packages/numpy/typing/tests/test_runtime.py": 12534621594420844770, + "venv/lib/python3.13/site-packages/pandas/core/sparse/api.py": 9094651265692218821, + "venv/lib/python3.13/site-packages/pandas/compat/pyarrow.py": 13693379998785127538, + "docs/api/html/group__test__create__valid.html": 7128826085418675137, + "venv/lib/python3.13/site-packages/authlib/oauth2/__init__.py": 13682931764896414722, + "frontend/src/routes/dashboards/health/+page.svelte": 17352747164048319238, + "frontend/build/_app/immutable/nodes/27.BMzHmoh8.js": 4795776133483978937, + "docs/api/html/group____is__valid__runtime__api__key.html": 911899575469202802, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/columns.py": 8488246354818721406, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/test_constructors.py": 9767900500541600486, + "frontend/src/routes/admin/roles/+page.svelte": 7687872545536151950, + "venv/lib/python3.13/site-packages/numpy/lib/tests/test_recfunctions.py": 16014503284544747867, + "backend/src/models/dataset_review_pkg/_profile_models.py": 7842646189215266585, + "docs/api/html/group____parse__csv__enum__list.html": 13696325195084668398, + "backend/src/core/task_manager/lifecycle.py": 266438331337244287, + "docs/api/html/group__test__get__dashboards__pagination__oversize.html": 18020532049469957534, + "docs/api/html/group__MapperTool.html": 2827767962983201486, + "docs/api/html/group__createADGroupMapping__Function.html": 2891869775354387407, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/modules/gh26920/two_mods_with_no_public_entities.f90": 12296500845667555672, + "venv/lib/python3.13/site-packages/pygments/lexers/configs.py": 7712797939104760185, + "venv/lib/python3.13/site-packages/cryptography/__init__.py": 641702726372647770, + "venv/lib/python3.13/site-packages/pandas/core/window/numba_.py": 1444123360986509769, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_groupby.py": 15721254196883995634, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/__init__.py": 13584649922093198724, + "venv/lib/python3.13/site-packages/anyio/abc/_streams.py": 8089057973257658396, + "docs/api/html/group__DashboardValidationPlugin.html": 4604274485935435783, + "venv/lib/python3.13/site-packages/ecdsa/errors.py": 11755424742691057760, + "docs/api/html/group__getStatusBadgeClass__Function.html": 12235710533832862804, + "venv/lib/python3.13/site-packages/authlib/integrations/flask_oauth2/requests.py": 14847842002253212098, + "venv/lib/python3.13/site-packages/pydantic/v1/env_settings.py": 6194387033830177219, + "venv/lib/python3.13/site-packages/pandas/tseries/api.py": 14785127136180503704, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/zookeeper.py": 4318643109309626098, + "docs/api/html/group__create__remote__repository.html": 1939587202210625796, + "frontend/build/_app/immutable/entry/start.vxsg0gSq.js": 387858028240834588, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/string_/__init__.py": 15130871412783076140, + "docs/api/html/group__TestAPIKeyRoutes.html": 4190666277064889784, + "venv/lib/python3.13/site-packages/numpy/_core/tests/_natype.py": 12013743717306900860, + "backend/src/api/routes/__tests__/test_clean_release_source_policy.py": 15459112324357199814, + "docs/api/html/group__DatasetReviewSchemaDtos.html": 4934342432845855674, + "docs/api/html/group__DictionaryResponse.html": 17051408790475508683, + "docs/api/html/group__PublicationService.html": 7781551738502534209, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_collections.py": 14307204206312829238, + "frontend/build/_app/immutable/nodes/1.kfjXM9_4.js": 5841310957836051002, + "venv/lib/python3.13/site-packages/PIL/_imagingft.pyi": 1385456134120145584, + "backend/src/services/dataset_review/repositories/session_repository.py": 793783444588945115, + "frontend/src/lib/components/translate/__tests__/TargetSchemaHint.test.js": 8659643137288661057, + "docs/api/html/group__test__generate__docs__env__not__found.html": 10999514980476456328, + "venv/lib/python3.13/site-packages/numpy/fft/_pocketfft.py": 6142062990691556076, + "docs/api/html/group__SupersetClientGetDashboardDetail.html": 4781513685980981790, + "docs/api/html/group__test__toggleSidebar__Function.html": 11685985194827077966, + "docs/api/html/group__MergeResolveRequest.html": 17150347272995916362, + "venv/lib/python3.13/site-packages/keyring/py.typed": 15130871412783076140, + "docs/api/html/group__ProfileUtils.html": 7099024507188160608, + "venv/lib/python3.13/site-packages/pip/_vendor/distlib/version.py": 6401479036177132374, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/proxy.py": 13008557041076030198, + "venv/lib/python3.13/site-packages/numpy/random/bit_generator.pxd": 14261507718442469988, + "docs/api/html/group__handleDeploy__Function.html": 16324615477613900974, + "dist/docker/ss-tools-backend.0.1.5.tar.xz": 11628671790222254361, + "docs/api/html/group____resolve__target__envs.html": 326825327677354212, + "docs/api/html/group__test__calculate__schedule__midnight__crossing.html": 6782803328672454391, + "docs/api/html/group__fetch__models.html": 1931603908808447028, + "venv/lib/python3.13/site-packages/websockets/http.py": 9406523480132204760, + "docs/api/html/group__compute__preview__fingerprint.html": 9160170929765714565, + "docs/api/html/search/groups_e.js": 12304992592735263012, + "docs/api/html/group__sync__dashboard.html": 8986119955907540350, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_dlpack.py": 7908102652282922952, + "backend/src/core/utils/network.py": 535557448085943960, + "docs/api/html/group__GitServiceGiteaMixin.html": 16505751774814321714, + "venv/lib/python3.13/site-packages/greenlet/greenlet_msvc_compat.hpp": 12504008513945919250, + "venv/lib/python3.13/site-packages/PIL/ImageEnhance.py": 3218405223202808764, + "docs/api/html/group__StoragePlugin.html": 1518633266255703440, + "venv/lib/python3.13/site-packages/pandas/core/arrays/integer.py": 7775055011080964709, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/test_easter.py": 3397863170068576196, + "venv/lib/python3.13/site-packages/httpcore/_async/socks_proxy.py": 9026788710898138090, + "docs/api/html/group__ConsolidatedSettingsResponse.html": 6466679869565868829, + "venv/lib/python3.13/site-packages/gitdb/db/__init__.py": 2615504918450276693, + "venv/lib/python3.13/site-packages/dateutil/zoneinfo/rebuild.py": 1813298167468704155, + "docs/api/html/group__ensure__encryption__key.html": 10633170068816212756, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/redis.py": 11313520438600819390, + "venv/lib/python3.13/site-packages/pandas/util/_exceptions.py": 14426860079366867683, + "venv/lib/python3.13/site-packages/pygments/lexers/_tsql_builtins.py": 829193091321611630, + "docs/api/html/group__GitServiceUrlMixin.html": 4060641084492531477, + "docs/api/html/group__DashboardDatasetItem.html": 288797829356699689, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/regression/mod_derived_types.f90": 2556756676983488922, + "backend/src/models/dataset_review_pkg/_session_models.py": 16972488060939570617, + "venv/lib/python3.13/site-packages/pygments/lexers/ul4.py": 9580182474451220952, + "docs/api/html/group__test__create__empty__permissions.html": 2212767387589401498, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/test_nat.py": 7400568652984772972, + "venv/lib/python3.13/site-packages/PIL/_version.py": 3025206805667405876, + "venv/lib/python3.13/site-packages/authlib/integrations/httpx_client/oauth1_client.py": 3129955464287331242, + "backend/src/api/routes/__tests__/test_migration_routes.py": 12273917934344716029, + "backend/src/services/security_badge_service.py": 18061005101062867126, + "docs/api/html/group__test__ensure__encryption__key__reuses__existing__env__file__value.html": 11987119925878454851, + "frontend/src/components/tools/MapperTool.svelte": 2340365930254575464, + "scripts/build_offline_docker_bundle.sh": 1393881756263325163, + "frontend/src/lib/api/translate/target-schema.js": 10665479411127476571, + "docs/api/html/group__SystemSettings.html": 4117500366250322872, + "venv/lib/python3.13/site-packages/authlib/oidc/core/claims.py": 2035724920423374302, + "docs/api/html/group__test__lookup__superset__accounts__env__not__found.html": 13190978675041730982, + "docs/api/html/group__cot__logger__instance.html": 13706182001628895155, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_dot.py": 16876560325283070437, + "frontend/e2e/run-enterprise-clean-e2e.sh": 11626901202531925046, + "venv/lib/python3.13/site-packages/pygments/styles/staroffice.py": 11694998942433220413, + "docs/api/html/group__get__param.html": 2682553762034055515, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/pass/dtype.py": 9342837412280377086, + "docs/api/html/group__test__delete__task__not__found.html": 11432183556776181332, + "venv/lib/python3.13/site-packages/numpy/_utils/__init__.pyi": 13530522898409268883, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_unicode.py": 1103608932321322989, + "venv/lib/python3.13/site-packages/pandas/core/indexes/datetimelike.py": 4356700619632833878, + "frontend/src/components/git/GitManager.svelte": 13017880964846202318, + "docs/api/html/globals_func_s.html": 12786975089129434154, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/mongodb.py": 5310116401542377882, + "venv/lib/python3.13/site-packages/websockets/legacy/server.py": 5390090919193784186, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_typst.py": 14619529283550788275, + "venv/lib/python3.13/site-packages/numpy/testing/tests/__init__.py": 15130871412783076140, + "venv/bin/pygmentize": 2166491742035772877, + "docs/api/html/group____authorize__intent.html": 14955083261995312080, + "docs/api/html/group__ConfigManager.html": 2939116672940219717, + "docs/api/html/group____perform__upload.html": 16003778061143161761, + "venv/lib/python3.13/site-packages/httpcore/_utils.py": 3255581092836273826, + "docs/api/html/group__test__init__repository__initializes__and__saves__binding.html": 17804439037629994394, + "venv/lib/python3.13/site-packages/yaml/nodes.py": 3463622922914760653, + "docs/api/html/group__update__migration__settings.html": 16961141318650899544, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/emath.pyi": 3718540478840842664, + "docs/api/html/group__test__returns__existing__banner.html": 12335002589831108201, + "docs/api/html/group__SupersetContextParsingMixin.html": 6860207674910724587, + "docs/api/html/group__build__summary.html": 14095423152800582584, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/search/all_6.js": 9451005297611312251, + "backend/src/services/clean_release/stages/data_purity.py": 7015962973560119808, + "venv/lib/python3.13/site-packages/passlib/tests/sample1b.cfg": 2872366861156549016, + "docs/api/html/group__get__sqllab__mappings.html": 10230808286410535014, + "docs/api/html/group__LoggerModule.html": 7322757663720393423, + "docs/api/html/search/functions_8.js": 3321647565030130819, + "docs/api/html/group__SupersetClientGetDashboard.html": 5538675640686031469, + "venv/lib/python3.13/site-packages/pandas/tests/test_register_accessor.py": 16661086720613729974, + "venv/lib/python3.13/site-packages/pygments/lexers/verification.py": 16645885125604874183, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_describe.py": 14493300276133670380, + "frontend/src/lib/stores/__tests__/mocks/env_public.js": 17950613826406014914, + "venv/lib/python3.13/site-packages/ecdsa/test_ecdsa.py": 17595025859105321562, + "backend/src/core/auth/api_key.py": 15898549010462339946, + "venv/lib/python3.13/site-packages/PIL/ContainerIO.py": 7551929914241379005, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_rename_axis.py": 13193478481758930128, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_sql.py": 3901697517087276687, + "venv/lib/python3.13/site-packages/pandas/io/sas/sas7bdat.py": 17851617851457266799, + "docs/api/html/group__TestContext.html": 16549518825764564714, + "docs/api/html/group__run__checks.html": 17159795815287064770, + "venv/lib/python3.13/site-packages/pygments/lexers/wowtoc.py": 17527844575999013592, + "docs/api/html/group__ValidationTaskCreate.html": 6710870961962534726, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/WHEEL": 18203500019759199992, + "docs/api/html/group__normalize__username.html": 9149179080201447778, + "docs/api/html/splitbar.png": 3792397713596451358, + "venv/lib/python3.13/site-packages/numpy/_core/arrayprint.pyi": 84606125837677197, + "docs/api/html/group__test__create__session.html": 9252520681669959841, + "venv/lib/python3.13/site-packages/numpy/matrixlib/__init__.py": 18402126171936308228, + "docs/api/html/group__InlineCorrectionSubmit.html": 13969595502641036335, + "venv/lib/python3.13/site-packages/itsdangerous/exc.py": 11181421963755661477, + "venv/lib/python3.13/site-packages/pandas/_config/display.py": 433650013567734821, + "venv/lib/python3.13/site-packages/gitdb/test/test_util.py": 541144638852210407, + "docs/api/html/group__request.html": 4526564378065461172, + "venv/lib/python3.13/site-packages/pandas/tests/series/indexing/test_delitem.py": 14336697139511118920, + "docs/api/html/group__archive__exports.html": 9704090435080462719, + "venv/lib/python3.13/site-packages/fastapi/_compat/shared.py": 5249003014594464718, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_function.py": 7048185308307653546, + "venv/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py": 7301817934902711510, + "venv/lib/python3.13/site-packages/PIL/_imagingcms.pyi": 12476347428820346383, + "venv/lib/python3.13/site-packages/fastapi-0.127.1.dist-info/REQUESTED": 15130871412783076140, + "backend/src/scripts/seed_permissions.py": 2299443594185631497, + "docs/api/html/group__test__resolve__trusted__policy__snapshots__missing__profile.html": 13528681884996041677, + "venv/lib/python3.13/site-packages/pandas/tests/io/test_gcs.py": 7733247456252965901, + "docs/api/html/group__frontend__src__lib__api__validation__js____buildParams.html": 8870797152431444795, + "venv/lib/python3.13/site-packages/apscheduler/executors/asyncio.py": 2230610508870117058, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_is_full.py": 9577318758326279694, + "venv/lib/python3.13/site-packages/pandas/tests/dtypes/cast/test_box_unbox.py": 7579587620530953711, + "docs/api/html/group__test__json__load__if__needed__dict.html": 1232447365145854146, + "venv/lib/python3.13/site-packages/jsonschema/tests/test_deprecations.py": 16367195310156359251, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/__init__.py": 697601512486720554, + "docs/api/html/navtreeindex0.js": 1729443049000642083, + "docs/api/html/group__APIClient.html": 1707825928665932322, + "venv/lib/python3.13/site-packages/_pytest/__init__.py": 3769333500910148571, + "venv/lib/python3.13/site-packages/numpy/_typing/_nbit_base.py": 7446626038222262489, + "backend/src/services/git/__init__.py": 18353922816391181902, + "venv/lib/python3.13/site-packages/pydantic_settings/version.py": 17263591404988984480, + "docs/api/html/doxygen.css": 8786819515706830953, + "docs/api/html/navtreeindex6.js": 6833292869822138239, + "venv/lib/python3.13/site-packages/packaging/utils.py": 3469957615920103670, + "docs/api/html/group__dry__run__migration.html": 12031214645025182651, + "venv/lib/python3.13/site-packages/pandas/core/indexes/extension.py": 7193879749426425951, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/__init__.py": 7526309124863950484, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/arrayobject.h": 4345432890284821918, + "docs/api/html/group__ProfileFixtures__Module.html": 11831895659314053968, + "docs/api/html/group____PluginLoaderStub.html": 11751810125582050067, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/style.py": 10665998898253460114, + "docs/api/html/group__abort__merge.html": 8636553700350043115, + "venv/lib/python3.13/site-packages/pip-25.1.1.dist-info/METADATA": 16700674024421191168, + "venv/lib/python3.13/site-packages/pyasn1-0.6.2.dist-info/METADATA": 11557342107649194148, + "venv/lib/python3.13/site-packages/jsonschema_specifications/schemas/draft201909/vocabularies/applicator": 6870991911243684132, + "venv/lib/python3.13/site-packages/numpy/_core/_operand_flag_tests.cpython-313-x86_64-linux-gnu.so": 12296711931541109624, + "venv/lib/python3.13/site-packages/httpx/_utils.py": 4836536051177895209, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/suite/test_dialect.py": 4700488504252803159, + "venv/lib/python3.13/site-packages/numpy/matrixlib/tests/test_numeric.py": 17391095502314929042, + "venv/lib/python3.13/site-packages/pygments/styles/borland.py": 17570558758932580659, + "docs/api/html/search/groups_5.js": 8198735349335934773, + "backend/alembic/versions/b1c2d3e4f5a6_add_source_hash_to_translation_records.py": 4775534557742900399, + "docs/api/html/group__editEnv__Function.html": 10218410065986469503, + "docs/api/html/group____build__context__keys.html": 703884142675670583, + "docs/api/html/group__update__global__settings.html": 14939400432665122240, + "docs/api/html/group__create__sub__context.html": 6446207849507974736, + "venv/lib/python3.13/site-packages/pandas/tests/test_take.py": 17628004130770326677, + "docs/api/html/group__test__api__create__job__422__missing__name.html": 13658018012548984469, + "docs/api/html/group__SemanticSourceDto.html": 10276129549826437552, + "venv/lib/python3.13/site-packages/numpy/_core/fromnumeric.py": 9409852116555866847, + "docs/api/html/group__test__plugin__loader__logger__calls__exist.html": 14504869611475287296, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_astype.py": 4023134352880775277, + "docs/api/html/group__get__preview__session.html": 12746493050692153788, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/refresh_token.py": 17927903035152872556, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_contains.py": 5273759677032634868, + "venv/lib/python3.13/site-packages/pandas/io/parsers/c_parser_wrapper.py": 5841064965672872661, + "docs/api/html/group__extract__error__context.html": 10063584913283401746, + "docs/api/html/group__test__provider__config.html": 10682630246046688970, + "docs/api/html/group__GitRouter.html": 4366531192868731265, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/modules.pyi": 14176187662591164602, + "frontend/src/routes/settings/MigrationMappingsTable.svelte": 574459541553883663, + "venv/lib/python3.13/site-packages/numpy/_utils/__init__.py": 7731065084036767848, + "venv/lib/python3.13/site-packages/attr/filters.py": 14156141488477438777, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/writeonly.py": 14213960431308620600, + "backend/tests/test_constants_audit_fixes.py": 1798496996952882053, + "docs/api/html/group__test__global__settings__propagates__storage.html": 7088086585393016607, + "venv/lib/python3.13/site-packages/cryptography/x509/base.py": 9124948989225782039, + "venv/lib/python3.13/site-packages/numpy/f2py/common_rules.pyi": 2104100025130695295, + "docs/api/html/group__TestRegressionGuard.html": 15882303187698666439, + "venv/lib/python3.13/site-packages/numpy/_core/shape_base.pyi": 10225243791643474943, + "docs/api/html/search/functions_b.js": 1263949438112498611, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimelike_/test_indexing.py": 1926721846569184112, + "frontend/src/lib/components/dataset-review/__tests__/us2_semantic_workspace.ux.test.js": 7475597145566385341, + "venv/lib/python3.13/site-packages/numpy/linalg/_umath_linalg.cpython-313-x86_64-linux-gnu.so": 17652852308919396403, + "docs/api/html/group__test__jinja__comment__excluded.html": 17442480642563518330, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/test_arithmetic.py": 11620920548943067949, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/methods/test_nth.py": 17151595277666975085, + "venv/lib/python3.13/site-packages/pygments/lexers/smithy.py": 10789533877863290582, + "docs/api/html/group__DashboardHub__getGitSummaryLabel__Function.html": 2221712068669890122, + "venv/lib/python3.13/site-packages/pygments/lexers/diff.py": 5930189593946259160, + "venv/lib/python3.13/site-packages/fastapi/testclient.py": 2606236077497278662, + "backend/src/services/clean_release/artifact_catalog_loader.py": 16917420865314497219, + "venv/lib/python3.13/site-packages/yaml/composer.py": 13619070314468518566, + "docs/api/html/group__UseGitManager.html": 11005313010951220315, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__frontend__src__lib__stores__datasetReviewSession__js____setDirty.html": 6652561854785184600, + "venv/lib/python3.13/site-packages/ecdsa/numbertheory.py": 13538480862642502330, + "venv/lib/python3.13/site-packages/pygments/lexers/tablegen.py": 2917626373516779857, + "docs/api/html/group__DEFAULT__MAX__OUTPUT__TOKENS.html": 7465573242087549666, + "docs/api/html/group__upload__file.html": 3260287998952682208, + "venv/lib/python3.13/site-packages/greenlet-3.3.0.dist-info/RECORD": 5169907629260560698, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/random/bitgen.h": 12421210767114721499, + "docs/api/html/group__TestResourceHubs.html": 18340295788009698355, + "docs/api/html/group__get__clarification__state.html": 5572805696675612675, + "venv/lib/python3.13/site-packages/anyio/to_thread.py": 12767326212338028587, + "docs/api/html/group__SupersetCompilationAdapter__mark__preview__stale.html": 7887865650371630867, + "docs/api/html/group__StorageService.html": 4742624646378331494, + "venv/lib/python3.13/site-packages/numpy/f2py/_src_pyf.pyi": 8916922961301555974, + "docs/api/html/group__MaintenanceStore.html": 3766820613250188744, + "venv/lib/python3.13/site-packages/pandas/tests/io/excel/test_readers.py": 2136472634307645842, + "venv/lib/python3.13/site-packages/pydantic/config.py": 1450073446492183677, + "docs/api/html/group__test__get__repository__status__propagates__non__404__http__exception.html": 2565945077269641492, + "docs/api/html/group__PublishRequest.html": 15758562664567551847, + "venv/lib/python3.13/site-packages/greenlet/tests/test_weakref.py": 6780204527427742510, + "docs/api/html/group__debug.html": 5353036165559255799, + "docs/api/html/group__SearchPluginModule.html": 11654743945648339497, + "venv/lib/python3.13/site-packages/itsdangerous/__init__.py": 6969210114978365656, + "venv/lib/python3.13/site-packages/httpcore/_sync/http11.py": 5947339024588907032, + "docs/api/html/navtreeindex19.js": 1947581984165981900, + "venv/lib/python3.13/site-packages/rapidfuzz/fuzz_cpp_avx2.cpython-313-x86_64-linux-gnu.so": 12574929384355254457, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/test_period.py": 3419297908709788013, + "venv/lib/python3.13/site-packages/tzlocal-5.3.1.dist-info/RECORD": 15574202654585730126, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/orderinglist.py": 10162099108656794338, + "docs/api/html/group__test__get__settings.html": 228135717549408768, + "frontend/src/routes/profile/__tests__/profile-preferences.integration.test.js": 10120412205695269578, + "docs/api/html/group__test__calculate__schedule__empty__list.html": 9256285575620728758, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_reindex.py": 8772423071604000084, + "docs/api/html/group__get__migration__settings.html": 13897329278259758640, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/mysqldb.py": 2138006249556140141, + "docs/api/html/group__test__get__datasets__pagination__oversize.html": 6499529865741709635, + "docs/api/html/group__create__api__key.html": 9694652778073542091, + "docs/api/html/group____ensure__translation__schedules__columns.html": 7448712454629073660, + "venv/lib/python3.13/site-packages/jeepney/__init__.py": 1063800617763464773, + "venv/lib/python3.13/site-packages/pandas/io/parsers/readers.py": 7899068668225389287, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mysql/reserved_words.py": 8022121780993497971, + "docs/api/html/group__ActivityTest__Module.html": 938547209662965960, + "docs/api/html/group__frontend__src__services__storageService__js____listFiles.html": 3465282820892681941, + "docs/api/html/group__update__column__description.html": 6565104927390241388, + "venv/lib/python3.13/site-packages/jsonschema_specifications-2025.9.1.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/ec.py": 8864112842559211971, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py": 16550134594771291234, + "frontend/build/_app/immutable/chunks/Cxl1pO2L.js": 1716319940315732175, + "docs/api/html/group__handleMappingResolve__Function.html": 2057230359464505849, + "docs/api/html/jquery.js": 16180958684071666265, + "docs/api/html/folderopend.svg": 12848593963915777968, + "frontend/playwright-report/trace/codeMirrorModule.DYBRYzYX.css": 14866915564324637173, + "docs/api/html/group__getStatusColor__Function.html": 5211543247156991189, + "docs/api/html/group__oauth.html": 12697823183291455723, + "venv/lib/python3.13/site-packages/pygments/lexers/stata.py": 7249891970174867363, + "docs/api/html/group__DashboardHub__getColumnCellValue__Function.html": 10318545865816410175, + "docs/api/html/group____load__dataset__review__context.html": 2812784457432782300, + "docs/api/html/search/groups_12.js": 10902160763074406551, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/RECORD": 9502524172853437600, + "venv/lib/python3.13/site-packages/numpy/_core/_ufunc_config.py": 7365943844063702766, + "frontend/src/lib/api/maintenance.js": 16247720324991780393, + "docs/api/html/group__test__list__tasks__service__error.html": 1075541340524463072, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildAssistantSeedPrompt.html": 4030604807548245084, + "docs/api/html/group__get__assistant__audit.html": 12289421674798192339, + "venv/lib/python3.13/site-packages/jsonschema/tests/typing/test_all_concrete_validators_match_protocol.py": 14824388798676578409, + "docs/api/html/group__taskLogs__Data.html": 10064937378577441554, + "docs/api/html/group__PageContracts.html": 8602909438030144707, + "docs/api/html/group__add__environment.html": 5721811152365136259, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/mypy/plugin.py": 16042078006552521613, + "docs/api/html/group__list__events.html": 1944931140844300363, + "docs/api/html/group____build__values__clause.html": 14938421019054450990, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/engine.py": 2652248727360250985, + "venv/lib/python3.13/site-packages/numpy/_core/_simd.pyi": 13553578040186939249, + "docs/api/html/group__handleTaskClick__Function.html": 11752031450688903634, + "docs/api/html/group__ValidationApi.html": 8585641606625752685, + "venv/lib/python3.13/site-packages/rsa/key.py": 4783269051050521475, + "venv/lib/python3.13/site-packages/_pytest/outcomes.py": 12782895027812738142, + "frontend/src/lib/stores/__tests__/mocks/stores.js": 12764819706469057535, + "backend/src/models/dataset_review_pkg/_filter_models.py": 6112699152675854683, + "docs/api/html/group__ComplianceStage.html": 17814341327770468741, + "venv/lib/python3.13/site-packages/PIL/MpegImagePlugin.py": 7726254767528154514, + "frontend/src/routes/settings/automation/+page.svelte": 13199339263146300897, + "backend/src/plugins/llm_analysis/scheduler.py": 16461360386665667203, + "venv/lib/python3.13/site-packages/numpy/_array_api_info.pyi": 13283968924732680159, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/loading.py": 3460142472461437916, + "docs/api/html/group____dataset__review__session.html": 17789442203180064219, + "docs/api/html/group__TestDashboardsApi.html": 5509739390736522040, + "venv/lib/python3.13/site-packages/click/formatting.py": 6694583529073729642, + "venv/lib/python3.13/site-packages/pandas/core/arrays/_mixins.py": 14293848457775268010, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_pipe.py": 8255917221095334212, + "venv/lib/python3.13/site-packages/greenlet/tests/test_generator_nested.py": 12822240347707346718, + "venv/lib/python3.13/site-packages/attr/_next_gen.py": 16353246941851015420, + "docs/api/html/group__test__get__job__not__found.html": 486301214235859142, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_alpha_unix.h": 2759424570606117942, + "venv/lib/python3.13/site-packages/numpy/_core/einsumfunc.pyi": 4729765666018003634, + "docs/api/html/group__getADGroupMappings__Function.html": 940667681674860937, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/oracle/provision.py": 9472538482404392519, + "venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py": 17216492046645992674, + "docs/api/html/group__TaskLogPersistenceService.html": 767164108519071447, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/INSTALLER": 17282701611721059870, + "frontend/src/components/PasswordPrompt.svelte": 2098675312823151283, + "backend/tests/core/test_git_service_gitea_pr.py": 11515414398155337212, + "venv/lib/python3.13/site-packages/fastapi/_compat/main.py": 13016492818231995760, + "docs/api/html/group__StartSessionRequest.html": 4207823078857319758, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-log.csv": 221209423454319618, + "venv/lib/python3.13/site-packages/ecdsa/test_ecdh.py": 3338834550392548171, + "venv/lib/python3.13/site-packages/git/objects/submodule/base.py": 10140490301897269395, + "docs/api/html/group____make__requests__http__error.html": 13378194080112951253, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_floats.py": 3064615530495290792, + "venv/lib/python3.13/site-packages/numpy/_distributor_init.py": 1101690714183863290, + "docs/api/html/group__SupersetClientBuildDatasetPreviewQueryContext.html": 18205655956081911529, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/numeric.pyi": 17005748821769179511, + "frontend/src/routes/profile/__tests__/profile-settings-state.integration.test.js": 5982682724976240528, + "venv/lib/python3.13/site-packages/fastapi/requests.py": 14587947416691824903, + "docs/api/html/group__GlobalSettings.html": 10632007378548267404, + "venv/lib/python3.13/site-packages/psycopg2_binary.libs/libkeyutils-dfe70bd6.so.1.5": 9414227432578104677, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/holiday/__init__.py": 15130871412783076140, + "docs/api/html/group__llm__provider.html": 10759065886221772665, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/index/__init__.py": 15130871412783076140, + "docs/api/html/group__SessionEventLogger.html": 3346105078868199944, + "backend/git_repos/remote/test-repo.git/HEAD": 12394318939628232491, + "venv/lib/python3.13/site-packages/secretstorage/exceptions.py": 16701299261122689924, + "docs/api/html/group__SidebarIntegrationTest__Module.html": 17744089971418699905, + "docs/api/html/group__test__init__falls__to__defaults.html": 6035220370511717887, + "docs/api/html/group__test__delete__logs__for__tasks.html": 8831029806986025325, + "docs/api/html/group__resolveEnvUrl__Function.html": 15828348202368551114, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_linux.h": 15772978805500070727, + "venv/lib/python3.13/site-packages/python_multipart/__init__.py": 10290895261363712562, + "backend/src/api/routes/dashboards/_listing_routes.py": 10092152880931335002, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/models.py": 17870758731501836303, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_liboffsets.py": 14705822637347212540, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/METADATA": 6438256145634479451, + "backend/src/services/reports/normalizer.py": 8463745489782355469, + "docs/api/html/group__TokenEstimator.html": 12075103575602311347, + "venv/lib/python3.13/site-packages/numpy/_core/cversions.py": 9724467539503410469, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_indexing.py": 11572911419313573163, + "docs/api/html/group__SupersetContextTemplatesMixin____collect__query__bearing__expressions.html": 17879749034024990070, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/styles/_mapping.py": 11752660241391232497, + "docs/api/html/group__RegisterCandidateRequest.html": 7180845738222833015, + "docs/api/html/group__test__run__detail__not__found.html": 5832122095347665056, + "venv/lib/python3.13/site-packages/numpy/random/_examples/cython/extending_distributions.pyx": 15302775787158991386, + "venv/lib/python3.13/site-packages/apscheduler/schedulers/gevent.py": 4811840397266785551, + "docs/api/html/globals_c.html": 11969223700440566246, + "venv/lib/python3.13/site-packages/uvicorn-0.40.0.dist-info/WHEEL": 7454950858448014158, + "docs/api/html/group__frontend__src__routes__datasets__review__review__workspace__helpers__js____buildImportMilestones.html": 8235208288594735263, + "venv/lib/python3.13/site-packages/pandas/core/indexes/category.py": 18192569199634406001, + "venv/lib/python3.13/site-packages/more_itertools/recipes.py": 5254043689078173976, + "venv/lib/python3.13/site-packages/PIL/GbrImagePlugin.py": 7681313333602257802, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_sort_values.py": 12064896795288302730, + "venv/lib/python3.13/site-packages/pygments/styles/manni.py": 11853576691280294949, + "venv/lib/python3.13/site-packages/h11-0.16.0.dist-info/INSTALLER": 17282701611721059870, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/multiindex/test_chaining_and_caching.py": 11784574265874608859, + "frontend/src/components/RepositoryDashboardGrid.svelte": 8427306073875543545, + "backend/src/core/superset_client/_datasets_preview_filters.py": 3588066821120121881, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/__init__.py": 3521833849634327520, + "docs/api/html/group__ExecutionMappingReview.html": 13336104515329815272, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc6749/grants/client_credentials.py": 256154392957334831, + "docs/api/html/group__missing__mandatory__stages.html": 7547931486255029352, + "venv/lib/python3.13/site-packages/gitdb/test/test_example.py": 3039934065817753241, + "venv/lib/python3.13/site-packages/PIL/SgiImagePlugin.py": 6916200065790425497, + "frontend/src/routes/settings/FeaturesSettings.svelte": 14192374301379217586, + "venv/lib/python3.13/site-packages/packaging/_tokenizer.py": 7442392076059866359, + "docs/api/html/group__SupersetDashboardsCrudMixin.html": 9711358306599693626, + "docs/api/html/group__create__gitea__repository.html": 8308843234769151760, + "docs/api/html/group__MappingsApi.html": 11117168124577566737, + "venv/lib/python3.13/site-packages/pip/_vendor/cachecontrol/cache.py": 3177663126928467950, + "venv/lib/python3.13/site-packages/_pytest/fixtures.py": 1442767529726551116, + "venv/lib/python3.13/site-packages/numpy/random/_common.pyi": 5094115692952609118, + "venv/lib/python3.13/site-packages/sqlalchemy/sql/default_comparator.py": 47118577003622736, + "frontend/build/_app/immutable/chunks/OaJiW-Rn.js": 4173693905736774548, + "venv/lib/python3.13/site-packages/uvicorn/protocols/http/h11_impl.py": 1419719055394402145, + "venv/lib/python3.13/site-packages/numpy/random/tests/data/generator_pcg64_np121.pkl.gz": 4708588000144933291, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/metrics_cpp.cpython-313-x86_64-linux-gnu.so": 10263790384087091349, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py": 16386268386376675175, + "venv/lib/python3.13/site-packages/pandas/tests/indexing/test_indexing.py": 5910217172351020811, + "venv/lib/python3.13/site-packages/jsonschema/_types.py": 9250091433305395923, + "docs/api/html/group__publish__candidate.html": 1676322270799999151, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_diff.py": 2216748778018882333, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/mssql/information_schema.py": 8551227132710897346, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/datetimes/test_cumulative.py": 6346947559103505068, + "docs/api/html/group__test__parsed__native__filters__empty.html": 14083649022229199477, + "venv/lib/python3.13/site-packages/numpy/core/defchararray.py": 11371045148957547088, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/base_class/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pydantic_core-2.41.5.dist-info/METADATA": 3044554568188160018, + "frontend/build/_app/immutable/nodes/36.CDydt-CG.js": 9799482772362304763, + "docs/api/html/group__TestTaskPersistenceHelpers.html": 8081661106396480485, + "venv/lib/python3.13/site-packages/git/repo/base.py": 13975465345838683533, + "venv/lib/python3.13/site-packages/pandas/tests/series/methods/test_nlargest.py": 14518132768753249360, + "docs/api/html/group__validate__timezone.html": 10510695123016011574, + "docs/api/html/group____EXT__frontend__AssistantChatTest__s.html": 10743878313235782900, + "venv/lib/python3.13/site-packages/pycparser/c_parser.py": 3954138286651284327, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/bar.py": 12859947592168238832, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/io/generate_legacy_storage_files.py": 11015954144633661960, + "docs/api/html/group__TestApprovalService.html": 4110308134003984759, + "docs/api/html/group__handleDrop__Function.html": 7991568720228574649, + "venv/lib/python3.13/site-packages/ecdsa/test_ellipticcurve.py": 11125783981574901613, + "venv/lib/python3.13/site-packages/_pytest/terminal.py": 885450969703476729, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/test_read_fwf.py": 13684564756628524738, + "venv/lib/python3.13/site-packages/pygments/lexers/business.py": 10410439210806697214, + "docs/api/html/group__SupersetClientGetDashboardsSummaryPage.html": 12787892430248800577, + "docs/api/html/group__DictionaryEntryCreate.html": 6174105871099793072, + "backend/git_repos/remote/test-repo.git/objects/a1/3525f5f44e26d8fbec3226bd488d14793575a2": 3742572048019652365, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/WHEEL": 16097436423493754389, + "docs/api/html/group__ApprovalDecision.html": 2897407194180760694, + "docs/api/html/group__test__get__dashboards__profile__filter__contract__owners__or__modified__by.html": 190248188377076802, + "docs/api/html/group__frontend__src__lib__api__maintenance__js____endMaintenance.html": 8307569460468971226, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_symbol.py": 10792387279408528886, + "frontend/src/routes/settings/+page.svelte": 13008569925433466788, + "docs/api/html/group__discover__declared__permissions.html": 14317626307828346282, + "docs/api/html/group__execute__migration.html": 4270006163430911897, + "frontend/src/routes/datasets/DatasetList.svelte": 12931773180198281284, + "docs/api/html/group__UiState__Store.html": 9990000106969800534, + "docs/api/html/group__test__editRow__Function.html": 7862387474531942746, + "docs/api/html/group__SupersetAuthCache__set.html": 15942031270979749240, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_meson.pyi": 2101854548535776875, + "docs/api/html/group__get__role__by__name.html": 15153180444289216375, + "venv/lib/python3.13/site-packages/jsonschema/_legacy_keywords.py": 16481666133229715822, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-tan.csv": 6233924374281534211, + "docs/api/html/group__goBackToList__Function.html": 7594473002237426910, + "venv/lib/python3.13/site-packages/authlib/jose/rfc7517/jwk.py": 6458001384702613165, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_duplicates.py": 9830936017197354309, + "frontend/src/lib/components/reports/__tests__/report_detail.integration.test.js": 13812015120538720344, + "frontend/src/routes/+page.svelte": 5863865830077771796, + "docs/api/html/group__handleRunMapper__Function.html": 4381627270655982198, + "venv/lib/python3.13/site-packages/anyio/streams/buffered.py": 8916188172110578587, + "venv/lib/python3.13/site-packages/rapidfuzz/_utils.py": 14374663810542207121, + "venv/lib/python3.13/site-packages/numpy/random/tests/test_smoke.py": 7476432112986643847, + "docs/api/html/group__delete__provider.html": 17051246773127866409, + "frontend/src/routes/dashboards/[id]/components/DashboardHeader.svelte": 767871726207047472, + "backend/git_repos/remote/test-repo.git/objects/48/c3fde4d5fff5bf98e9bcaa5f328b199aa18292": 6121250272621110311, + "docs/api/html/group__LaunchDatasetResponse.html": 17085095085234548738, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/provision.py": 7362363963464621570, + "venv/lib/python3.13/site-packages/websockets/uri.py": 264376168984862773, + "docs/api/html/group__TaskContextModule.html": 16641711360891107456, + "venv/lib/python3.13/site-packages/attr/_version_info.pyi": 7141309436445210354, + "docs/api/html/group__upsert__clarification__finding.html": 8857419435924114441, + "docs/api/html/group__frontend__src__routes__settings__settings__utils__js____normalizeTab.html": 15048396624822097099, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/mypy.ini": 4763385838822685881, + "venv/lib/python3.13/site-packages/packaging/py.typed": 15130871412783076140, + "docs/api/html/group__test__build__dataset__preview__query__context__merges__dataset__template__params__and__preserves__user__values.html": 13508623923930235675, + "docs/api/html/group__test__register__candidate__rejects__empty__artifact__set.html": 16268783415250759980, + "docs/api/html/group__SupersetUserProjectionMixin.html": 8962253689270385612, + "frontend/build/_app/immutable/chunks/DsnmJJEf.js": 17416107759626312632, + "docs/api/html/group__downloadFileUrl__Function.html": 10917163696944210952, + "docs/api/html/group____seed__repository.html": 7316844014942440184, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_subclass.py": 10949229764696473939, + "venv/lib/python3.13/site-packages/authlib/common/encoding.py": 52296066492753953, + "docs/api/html/group____task.html": 14186673195894587117, + "venv/lib/python3.13/site-packages/pygments/lexers/snobol.py": 414946539602526513, + "docs/api/html/group____do__http__request.html": 5541890670779429458, + "docs/api/html/globals_func.html": 10725622132015229383, + "docs/api/html/group__require__api__key__or__jwt.html": 452602241730021960, + "docs/api/html/group__test__continue__merge__passes__message__and__returns__commit.html": 7335459609919818162, + "venv/lib/python3.13/site-packages/pandas/tests/series/accessors/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/pygments/styles/trac.py": 13968346768164381833, + "venv/lib/python3.13/site-packages/starlette/convertors.py": 17839747154274354605, + "docs/api/html/group__test__persist__task__with__logs.html": 3561488143169471878, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/align.py": 6078666505647454830, + "venv/lib/python3.13/site-packages/h11/_connection.py": 6971907312737339559, + "docs/api/html/group__DebugPlugin.html": 16864972402244728207, + "docs/api/html/group__loadBranches__Function.html": 2222762227047931041, + "docs/api/html/group__resolve__task.html": 7907422604422526815, + "docs/api/html/group__DashboardHub__normalizeOwners__Function.html": 11075995940263529029, + "docs/api/html/group__GitInitPanel.html": 15661107424053425656, + "docs/api/html/group__TraceContextMiddleware__dispatch.html": 11355825183050222114, + "venv/lib/python3.13/site-packages/fastapi/templating.py": 10760135144345238340, + "docs/api/html/group__test__get__reports__filter__and__pagination.html": 15756423594624112678, + "docs/api/html/group__DatabaseModule.html": 14601049603487173809, + "venv/lib/python3.13/site-packages/jose/backends/base.py": 12795468809472351388, + "venv/lib/python3.13/site-packages/pydantic/deprecated/tools.py": 16466182352694741629, + "docs/api/html/group__test__success.html": 17787477233526904556, + "docs/api/html/group__isRepositoryReady__Function.html": 3411912494396355874, + "venv/lib/python3.13/site-packages/pip/_vendor/truststore/_macos.py": 7028037591427052891, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/methods/test_tz_convert.py": 13349243029437881949, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_names.py": 10037208029090713121, + "venv/lib/python3.13/site-packages/pygments/lexers/matlab.py": 1880915927722177654, + "docs/api/html/group__DictionaryCRUD.html": 6797497851497770225, + "venv/lib/python3.13/site-packages/pygments/lexers/perl.py": 17014528337020572681, + "frontend/playwright-report/trace/sw.bundle.js": 10879347794748701702, + "frontend/src/components/backups/BackupManager.svelte": 10161959141846123102, + "venv/lib/python3.13/site-packages/pandas/core/groupby/__init__.py": 3015263524218708013, + "backend/src/core/superset_client/_base.py": 8704044125468217140, + "docs/api/html/group__normalize__user__payload.html": 7049600760546559055, + "venv/lib/python3.13/site-packages/websockets-15.0.1.dist-info/RECORD": 17985800801793506250, + "backend/src/services/__init__.py": 15123879863527585264, + "docs/api/html/group__ReportsApiTest.html": 634823893276138685, + "docs/api/html/group__Counter.html": 5547720716432013609, + "backend/src/services/reports/__init__.py": 11818812865847046400, + "venv/lib/python3.13/site-packages/passlib/tests/test_ext_django.py": 3210697476292094384, + "venv/lib/python3.13/site-packages/pygments/lexers/idl.py": 11454152060351801696, + "frontend/src/lib/components/dataset-review/CompiledSQLPreview.svelte": 1996807488169542658, + "venv/lib/python3.13/site-packages/pandas/tests/io/json/test_json_table_schema_ext_dtype.py": 13021437050542691118, + "docs/api/html/group____build__object__diff.html": 13370696190761975058, + "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators_v1.py": 16723743964226526155, + "docs/api/html/group__frontend__src__routes__settings____page__ts____load.html": 17232811554336351206, + "venv/lib/python3.13/site-packages/pydantic_core/core_schema.py": 8106298745123589494, + "backend/tests/scripts/test_clean_release_tui.py": 9562190170888970200, + "docs/api/html/group__TranslationRunProgress.html": 17186985638749776681, + "venv/lib/python3.13/site-packages/pandas/core/shared_docs.py": 2173578869755325502, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_validate_kwargs.py": 10173906126211473463, + "docs/api/html/group__PluginConfig.html": 11629742848862930687, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py": 7624316342162916928, + "backend/src/api/routes/clean_release.py": 9890311709059474429, + "docs/api/html/group__handleSaveUser__Function.html": 4104256279914165704, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_to_timestamp.py": 12253372102602993034, + "frontend/src/lib/ui/Button.svelte": 14815370250842191735, + "venv/lib/python3.13/site-packages/click/core.py": 16793133181718891193, + "frontend/build/_app/immutable/assets/GitManager.BK73e3S1.css": 18342316651340870836, + "venv/lib/python3.13/site-packages/pip/_internal/network/lazy_wheel.py": 12936988990566680659, + "docs/api/html/group__test__delete__task__runs__default__false.html": 12317988397081006520, + "venv/lib/python3.13/site-packages/numpy/polynomial/tests/test_classes.py": 1440926036579216168, + "venv/lib/python3.13/site-packages/jeepney/tests/test_wrappers.py": 4813328996575597980, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc8628/models.py": 16636119441941357281, + "venv/lib/python3.13/site-packages/git/remote.py": 1694919255653049944, + "docs/api/html/group__get__job.html": 11838854381760893535, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timedelta/test_constructors.py": 1697610790604400105, + "docs/api/html/group__AdminService.html": 15757284534026835957, + "venv/lib/python3.13/site-packages/apscheduler-3.11.2.dist-info/INSTALLER": 17282701611721059870, + "backend/tests/conftest.py": 8326476333304129421, + "venv/lib/python3.13/site-packages/authlib/oauth2/rfc7662/introspection.py": 14645217065508644154, + "venv/lib/python3.13/site-packages/anyio/_core/_testing.py": 14024940365851109155, + "venv/lib/python3.13/site-packages/gitdb-4.0.12.dist-info/RECORD": 9803861100978586236, + "venv/lib/python3.13/site-packages/greenlet/_greenlet.cpython-313-x86_64-linux-gnu.so": 6036818030377883416, + "docs/api/html/tab_bd.png": 7964491274183929560, + "venv/lib/python3.13/site-packages/cryptography/hazmat/primitives/asymmetric/rsa.py": 7852235015155694076, + "venv/lib/python3.13/site-packages/apscheduler/jobstores/rethinkdb.py": 16413299263077970925, + "docs/api/html/group__test__cli__release__gate__commands__scaffold.html": 4903992815060946212, + "venv/lib/python3.13/site-packages/pydantic/_internal/_validate_call.py": 4078236486720616395, + "venv/lib/python3.13/site-packages/numpy/ma/tests/test_regression.py": 12593643817685777170, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/interval/test_overlaps.py": 8287716909793575347, + "frontend/src/components/git/CommitHistory.svelte": 6419514228971332714, + "docs/api/html/group__list__dashboard__banners.html": 14482370673488154596, + "venv/lib/python3.13/site-packages/git/objects/tag.py": 7954808535585493300, + "venv/lib/python3.13/site-packages/authlib/oidc/core/grants/hybrid.py": 14287665090144592501, + "backend/src/api/routes/translate/_job_routes.py": 11286107259925736168, + "docs/api/html/group__frontend__src__lib__stores__taskDrawer__js____openDrawer.html": 1057387600319463508, + "venv/lib/python3.13/site-packages/greenlet/platform/switch_ppc_macosx.h": 3653129798702716366, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_droplevel.py": 2071197388394658690, + "docs/api/html/group__get__clean__release__facade.html": 9736682632079627043, + "models": 14346352665111990852, + "venv/lib/python3.13/site-packages/urllib3/http2/connection.py": 1661620860881397554, + "docs/api/html/group__preview__translation.html": 3914934483187354741, + "venv/lib/python3.13/site-packages/pip/_vendor/idna/py.typed": 15130871412783076140, + "docs/api/html/group__test__revoked__key__raises__401.html": 10905401151898512037, + "venv/lib/python3.13/site-packages/anyio/_core/_contextmanagers.py": 1258491775823080427, + "docs/api/html/group__DatasetReviewSession.html": 6474466834876618927, + "venv/lib/python3.13/site-packages/pandas/tests/io/pytables/test_time_series.py": 5205199092955498507, + "docs/api/html/group__test__filter__state__model.html": 17888526114492502641, + "docs/api/doxygen_docs.h": 7306291205722937283, + "docs/api/html/group__test__props__definition.html": 1900076617459161710, + "venv/lib/python3.13/site-packages/pandas/io/parsers/base_parser.py": 15148258424429418380, + "docs/api/html/group__UpdateExecutionMappingRequest.html": 12143869237726717989, + "docs/api/html/group__test__get__dashboards__profile__filter__no__match__results__contract.html": 6248644768287182615, + "docs/api/html/group__test__sql__lab__launch__falls__back__to__legacy__execute__endpoint.html": 3611882124869363, + "venv/lib/python3.13/site-packages/pandas/core/indexes/accessors.py": 14394872772838047912, + "venv/lib/python3.13/site-packages/python_multipart/multipart.py": 5677735019292537174, + "venv/lib/python3.13/site-packages/pandas/errors/__init__.py": 13444371880185740908, + "docs/api/html/group__Route__Group__Contracts.html": 11731434895920260883, + "docs/api/html/group__get__run__detail.html": 4564623491331898259, + "docs/api/html/group__ArtifactCatalogLoader.html": 15778534404992178161, + "venv/lib/python3.13/site-packages/pydantic/_internal/_decorators.py": 9928398334052202841, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/period/methods/test_asfreq.py": 12721047184483392773, + "venv/lib/python3.13/site-packages/pydantic/_internal/_generics.py": 16438159042939059717, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_scalar_compat.py": 10068989979144965125, + "venv/lib/python3.13/site-packages/pandas/tests/extension/base/setitem.py": 9552868454186447925, + "venv/lib/python3.13/site-packages/pandas/io/formats/templates/html_table.tpl": 17198355067645888735, + "docs/api/html/group__MaintenanceEventStatus.html": 8449077302738724154, + "venv/lib/python3.13/site-packages/urllib3-2.6.2.dist-info/METADATA": 2624107638694809013, + "docs/api/html/group____normalize__filter__values.html": 7790645858095652627, + "docs/api/html/group__get__merge__conflicts.html": 13270034822172825745, + "venv/lib/python3.13/site-packages/pip/_vendor/tomli_w/__init__.py": 12849371269359608058, + "docs/api/html/group__test__batch__detect.html": 9632306453893154983, + "venv/lib/python3.13/site-packages/certifi-2025.11.12.dist-info/licenses/LICENSE": 14230156892574098522, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/util.py": 2866856507760047219, + "docs/api/html/group__BusinessSummarySource.html": 1094425953916303548, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_arrayprint.py": 13335063050941743734, + "backend/src/models/assistant.py": 8004226248612183587, + "docs/api/html/group__DatasetReviewMappingModels.html": 18079997751810017590, + "docs/api/html/group__launch__dataset.html": 8280088988730100751, + "venv/lib/python3.13/site-packages/pygments/formatters/svg.py": 11686316844190214147, + "venv/lib/python3.13/site-packages/numpy/lib/tests/data/py3-objarr.npz": 12369168483236619421, + "venv/lib/python3.13/site-packages/passlib/handlers/django.py": 66638338316838351, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/cacert.pem": 15624463015628939254, + "docs/api/html/group__create__access__token.html": 2049056774682954094, + "docs/api/html/group____ensure__user__dashboard__preferences__health__columns.html": 14728727752602035739, + "docs/api/html/navtreeindex18.js": 12393713505269071662, + "venv/lib/python3.13/site-packages/urllib3/util/url.py": 15992369651965016538, + "docs/api/html/group__get__task.html": 2469252830729210519, + "venv/lib/python3.13/site-packages/authlib/integrations/django_oauth2/signals.py": 10390443713463805090, + "venv/lib/python3.13/site-packages/numpy/lib/_index_tricks_impl.py": 13574106461973044500, + "venv/lib/python3.13/site-packages/pandas/tests/frame/methods/test_quantile.py": 11273403743157110261, + "venv/lib/python3.13/site-packages/pandas/tests/util/test_shares_memory.py": 2911029123657970431, + "venv/lib/python3.13/site-packages/requests/__init__.py": 2604491098990438229, + "docs/api/html/group__test__api__list__jobs.html": 9050185050148987988, + "venv/lib/python3.13/site-packages/pydantic-2.12.5.dist-info/REQUESTED": 15130871412783076140, + "venv/lib/python3.13/site-packages/httpx/_transports/asgi.py": 12335675489870745729, + "backend/src/services/clean_release/__tests__/test_audit_service.py": 10901982516436251314, + "docs/api/html/deprecated.html": 7802050220531259948, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh26681.f90": 1515497186927052106, + "venv/lib/python3.13/site-packages/numpy/tests/test_lazyloading.py": 17465787495701737655, + "docs/api/html/group__ValidationTaskService.html": 42675372436809990, + "venv/lib/python3.13/site-packages/cffi-2.0.0.dist-info/licenses/LICENSE": 17675192170670564526, + "docs/api/html/group__run__reject.html": 17845346756984073576, + "docs/api/html/group__confirmAssistantOperation__Function.html": 12036132904812216461, + "venv/lib/python3.13/site-packages/passlib/tests/test_hosts.py": 8284535274820610802, + "docs/api/html/group__ManifestService.html": 17042827860507544398, + "venv/lib/python3.13/site-packages/PIL/_util.py": 17116054813914373606, + "venv/lib/python3.13/site-packages/pydantic/plugin/_loader.py": 792663962985957671, + "venv/lib/python3.13/site-packages/pydantic/_internal/_schema_gather.py": 3634138701088855517, + "docs/api/html/navtreeindex17.js": 16370759160904060398, + "venv/lib/python3.13/site-packages/numpy/_core/tests/data/umath-validation-set-cbrt.csv": 8929553891827331780, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/offsets.cpython-313-x86_64-linux-gnu.so": 6726636942028566415, + "frontend/public/vite.svg": 13568221685541582385, + "venv/lib/python3.13/site-packages/ecdsa/test_malformed_sigs.py": 2606295770491528880, + "docs/api/html/group__frontend__src__services__taskService__js____getTask.html": 17745233273406842327, + "venv/lib/python3.13/site-packages/cryptography/hazmat/bindings/_rust/openssl/x448.pyi": 11733449165276720901, + "venv/lib/python3.13/site-packages/pip/_internal/commands/help.py": 4175797322922912326, + "venv/lib/python3.13/site-packages/numpy/tests/test_numpy_config.py": 3636377988901907424, + "venv/lib/python3.13/site-packages/pip/_vendor/pygments/lexers/python.py": 17886036670342116501, + "docs/api/html/group__PreviewAcceptResponse.html": 2553928199035482431, + "venv/lib/python3.13/site-packages/sqlalchemy/testing/pickleable.py": 7712040860601343075, + "venv/lib/python3.13/site-packages/pip/_vendor/platformdirs/unix.py": 13808187640638677050, + "venv/lib/python3.13/site-packages/httpcore/_backends/trio.py": 8394426709857317737, + "venv/lib/python3.13/site-packages/fastapi/_compat/__init__.py": 12380131786677943349, + "venv/lib/python3.13/site-packages/uvicorn/logging.py": 12858188863836778374, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_searchsorted.py": 11161416745923343741, + "venv/lib/python3.13/site-packages/pygments/lexers/tal.py": 447505725492547461, + "frontend/src/lib/stores/__tests__/mocks/state.js": 10691107564567912815, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/_pick.py": 15760492005471315814, + "venv/lib/python3.13/site-packages/_pytest/setuponly.py": 10959102379908419762, + "venv/lib/python3.13/site-packages/pandas/core/strings/accessor.py": 14021462534679430426, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/test_numeric_only.py": 13610631681447961840, + "venv/lib/python3.13/site-packages/git/objects/base.py": 11498407361286170399, + "frontend/src/lib/components/translate/TargetTabForm.svelte": 3058548789208976020, + "docs/api/html/group____get__or__init__decisions__store.html": 15627912227755842908, + "docs/api/html/group__get__repository__diff.html": 13880615963280114333, + "docs/api/html/group__test__jwt__precedence.html": 11405311010293398777, + "venv/lib/python3.13/site-packages/pillow.libs/libtiff-295fd75c.so.6.2.0": 17866578820357536656, + "venv/lib/python3.13/site-packages/pandas/core/window/common.py": 17956497339875932377, + "docs/api/html/group__ReportDetailIntegrationTest__Module.html": 11014134516097175898, + "docs/api/html/group__ValidationRecord.html": 5281046073662609348, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/test_ops.py": 14420421526781215500, + "docs/api/html/group__test__prefix__length.html": 10686431834265921774, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/floating/test_construction.py": 6564829381134214401, + "docs/api/html/group__test__get__database__mappings__success.html": 6411080375632795387, + "venv/lib/python3.13/site-packages/authlib/integrations/base_client/errors.py": 6538052228921426956, + "docs/api/html/group__AsyncAPIClient____get__auth__lock.html": 17454231653564423743, + "docs/api/html/group__ConfidenceState.html": 10944304052724522934, + "docs/api/html/group__TestMappingService.html": 11609065687830081419, + "docs/api/html/group____resolve__provider__id.html": 8297130391733338866, + "docs/api/html/closed.png": 16898092673784447083, + "venv/lib/python3.13/site-packages/httpcore/_async/http2.py": 16350817785499681224, + "docs/api/html/group__isAdminUser__Function.html": 2409880753894519871, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/auth.py": 912521029864094489, + "docs/api/html/group__RegistryStatus.html": 11698693702774238014, + "docs/api/html/group__test__i18n__usage.html": 8090230676674799070, + "venv/lib/python3.13/site-packages/charset_normalizer/utils.py": 11724147997933584095, + "docs/api/html/group__build__launch__blockers.html": 7838341297599392615, + "venv/lib/python3.13/site-packages/pip/_internal/index/__init__.py": 14829074315307489628, + "docs/api/html/group__openCreateModal__Function.html": 8404560973305150574, + "venv/lib/python3.13/site-packages/numpy/f2py/__main__.py": 12567751646090102167, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh18335.f90": 1126438609189294255, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/nbit_base_example.pyi": 8571452382806614924, + "docs/api/html/group__SupersetLookupService.html": 18424905745600548459, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_copy_deprecation.py": 16045426957636349035, + "venv/lib/python3.13/site-packages/pandas/core/arrays/sparse/accessor.py": 8373575175447947401, + "venv/lib/python3.13/site-packages/sqlalchemy/engine/result.py": 12262298207886620908, + "venv/lib/python3.13/site-packages/passlib/tests/test_utils_md4.py": 15270721044910698449, + "docs/api/html/group__logout.html": 16684691150600527260, + "venv/lib/python3.13/site-packages/numpy/lib/_twodim_base_impl.py": 4154852745131464543, + "docs/api/html/group____build__mock__stdscr.html": 18188773364932506767, + "venv/lib/python3.13/site-packages/pycparser/ply/lex.py": 3393278208465358140, + "docs/api/html/group__reports.html": 6554651341588172263, + "frontend/build/_app/immutable/chunks/YoM7MN62.js": 5599909526186837772, + "docs/api/html/group__router.html": 4047834241221489998, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/ndarray_misc.pyi": 7544619194032232861, + "venv/lib/python3.13/site-packages/numpy/random/__init__.pxd": 7355772669172093814, + "docs/api/html/group__APIKeyUtilities.html": 5445365654414296643, + "venv/lib/python3.13/site-packages/numpy/f2py/_backends/_backend.pyi": 13083543811193933305, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_deprecations.py": 6015734648984591027, + "frontend/build/favicon.svg": 6451919037497541980, + "venv/lib/python3.13/site-packages/numpy/tests/test_ctypeslib.py": 9592016432737074765, + "docs/api/html/group__NavigationMock.html": 10866560030441854001, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/timestamp/test_timezones.py": 8042029178538467146, + "docs/api/html/group__DashboardSelection.html": 1501151870893618877, + "venv/lib/python3.13/site-packages/pygments/styles/stata_light.py": 16319330015324455277, + "frontend/build/_app/immutable/chunks/C_cJ1BSS.js": 9136993653772081831, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/test_console.py": 11657469008721907972, + "venv/lib/python3.13/site-packages/numpy/_core/_add_newdocs.pyi": 9282436354926617898, + "docs/api/html/group__provider__config__edit__contract__tests__Function.html": 15824301754871063235, + "docs/api/html/group__run__compliance__status.html": 18273707357208548255, + "venv/bin/activate.fish": 5275961582873298085, + "venv/lib/python3.13/site-packages/git/index/__init__.py": 7874728461616959441, + "venv/lib/python3.13/site-packages/pygments/lexers/futhark.py": 10102535873433759899, + "venv/lib/python3.13/site-packages/websockets/speedups.c": 11800200877763464246, + "venv/lib/python3.13/site-packages/urllib3/util/response.py": 6106698559175827207, + "docs/api/html/group__frontend__src__services__taskService__js____resumeTask.html": 2730926071021487540, + "docs/api/html/group__test__git__config.html": 14894041378515335606, + "venv/lib/python3.13/site-packages/attr/_compat.py": 4016698632899969390, + "venv/lib/python3.13/site-packages/_pytest/warnings.py": 8633394774569709013, + "docs/api/html/group__updateRole__Function.html": 935827486794561356, + "frontend/src/lib/components/assistant/__tests__/assistant_clarification.integration.test.js": 15614617116241362179, + "docs/api/html/group__MigrateRequest.html": 12258712854787042018, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/__init__.py": 5933622200264572885, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/sparse/test_combine_concat.py": 3201068805128232483, + "docs/api/html/group__test__log__methods__delegation.html": 9316206776804683442, + "venv/lib/python3.13/site-packages/rapidfuzz/__init__.py": 12462153783886855740, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_numba.py": 1548992314517942530, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/scalars.pyi": 17683675356659300769, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_version.pyi": 3636944488233043883, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_regression.py": 16293647824734937323, + "venv/lib/python3.13/site-packages/pip/_vendor/certifi/core.py": 2300277198409164253, + "docs/api/html/group__get__auth__db.html": 7153540610662793603, + "venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/style.py": 15214295655795348821, + "venv/lib/python3.13/site-packages/httpx/_models.py": 6516278895118988231, + "venv/lib/python3.13/site-packages/authlib/integrations/__init__.py": 15130871412783076140, + "docs/api/html/group__SupersetClientGetDashboardPermalinkState.html": 14465481522755672093, + "venv/lib/python3.13/site-packages/numpy/lib/_stride_tricks_impl.py": 5260748170237305627, + "venv/lib/python3.13/site-packages/sqlalchemy/ext/associationproxy.py": 7421910137064160936, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexerrors.py": 4434868847540549873, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/conftest.py": 25473177671450048, + "venv/lib/python3.13/site-packages/numpy/_core/tests/test_indexing.py": 9988098331562906950, + "venv/lib/python3.13/site-packages/pandas/tests/generic/test_frame.py": 9378623452601244234, + "venv/lib/python3.13/site-packages/rpds/py.typed": 15130871412783076140, + "venv/lib/python3.13/site-packages/pandas/tests/base/test_constructors.py": 2439700578107653984, + "venv/lib/python3.13/site-packages/referencing/exceptions.py": 2535322334430110414, + "docs/api/html/group__BranchCheckout.html": 659397655908615531, + "docs/api/html/group__TargetSchemaValidation.html": 4385079790289560163, + "venv/lib/python3.13/site-packages/passlib/crypto/_blowfish/base.py": 15872835330892203433, + "frontend/build/_app/immutable/nodes/30.YMrF8Qt_.js": 2343255908605463459, + "docs/api/html/group__fetchRunDetail__Function.html": 6020648800343509598, + "venv/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/psycopg.py": 8023549900603235601, + "venv/lib/python3.13/site-packages/packaging/__init__.py": 5479277484104486860, + "venv/lib/python3.13/site-packages/pandas/tests/io/formats/style/test_to_latex.py": 2628895559914914693, + "docs/api/html/group__SupersetClientResolveTargetIdForDelete.html": 16317999718630461982, + "docs/api/html/group__ProfileModels.html": 9971230327622812343, + "docs/api/html/group__frontend__src__lib__stores__maintenance__svelte__js____loadDashboardBanners.html": 5809931002821855635, + "run.sh": 685807895772494419, + "docs/api/html/group__get__job__metrics.html": 440506911200266424, + "venv/lib/python3.13/site-packages/pandas/tests/plotting/frame/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/lib_utils.pyi": 3708905060001289515, + "docs/api/html/group__test__approve__rejects__foreign__report.html": 3963674165080312460, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/test_abstract_interface.py": 17625870734108222276, + "venv/lib/python3.13/site-packages/uvicorn/supervisors/__init__.py": 15971002342788501945, + "docs/api/html/search/functions_f.js": 17213773482888625604, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/util/__init__.py": 293004106962245584, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/datetimes/methods/test_resolution.py": 9651416588266430520, + "venv/lib/python3.13/site-packages/pandas/tests/tseries/offsets/common.py": 2075178842766962782, + "venv/lib/python3.13/site-packages/rsa-4.9.1.dist-info/LICENSE": 8423899413955829149, + "venv/lib/python3.13/site-packages/pandas/tests/io/xml/__init__.py": 15130871412783076140, + "docs/api/html/group__RoleSchema.html": 3555001744310629858, + "docs/api/html/group__RunSourceFetcher.html": 14682302550003967669, + "venv/lib/python3.13/site-packages/pytest-9.0.2.dist-info/METADATA": 3618776395694990076, + "docs/api/html/group__test__recover__imported__filters__preserves__unmatched__raw__native__filter__ids.html": 1109218303971466328, + "venv/lib/python3.13/site-packages/jeepney/io/tests/test_blocking.py": 6312017389529916004, + "docs/api/html/group__DictionariesPage.html": 10830021443332069379, + "venv/lib/python3.13/site-packages/greenlet/__init__.py": 3447613556981242475, + "venv/lib/python3.13/site-packages/numpy/tests/test_public_api.py": 11741922758622705449, + "frontend/src/lib/api/datasetReview.js": 11780670121350173465, + "docs/api/html/group__verify__password.html": 16868760447655092510, + "venv/lib/python3.13/site-packages/cffi/commontypes.py": 11198107299826138465, + "venv/lib/python3.13/site-packages/packaging-26.0.dist-info/licenses/LICENSE": 4274653168153720331, + "venv/lib/python3.13/site-packages/PIL/DdsImagePlugin.py": 10646832373516499548, + "docs/api/html/group____build__session__version__conflict__http__exception.html": 16116500469408421341, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/boolean/test_repr.py": 7552782621374439569, + "venv/lib/python3.13/site-packages/pandas/tests/window/test_rolling_quantile.py": 4225791949463893856, + "backend/ss_tools_dev.db": 15130871412783076140, + "docs/api/html/group____project__dashboard__response__items.html": 4840220165425219906, + "venv/lib/python3.13/site-packages/rapidfuzz/distance/Levenshtein.py": 15426828792217499138, + "venv/lib/python3.13/site-packages/pandas/_libs/tslibs/timestamps.pyi": 51728851886466199, + "docs/api/html/group__SupersetClientGetDatasets.html": 9914093133934169605, + "venv/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py": 5951912505933673835, + "docs/api/html/group__DashboardMetadata.html": 3536240584361293053, + "docs/api/html/group__getTasks__Function.html": 1086888913183663785, + "venv/lib/python3.13/site-packages/numpy/polynomial/hermite_e.pyi": 13068936038543812082, + "venv/lib/python3.13/site-packages/numpy/f2py/tests/src/callback/gh25211.f": 1486878017876900890, + "venv/lib/python3.13/site-packages/pygments/lexers/json5.py": 9736695186548899403, + "docs/api/html/group__test__submit__correction__overwrite.html": 5210632411258869706, + "docs/api/html/group__test__api__datasource__columns__bad__env.html": 6521752800637051274, + "venv/lib/python3.13/site-packages/anyio/_core/_eventloop.py": 6853955946028131785, + "frontend/build/_app/immutable/chunks/iUOTcjfh.js": 15827580985497395671, + "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/__init__.py": 15130871412783076140, + "docs/api/html/group__test__add__logs__single.html": 2862947647775846235, + "docs/api/html/group__TranslateSchemas.html": 17296673720360831542, + "docs/api/html/group__get__permission__by__id.html": 9650598973405100772, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/interval/test_constructors.py": 979131987837682435, + "docs/api/html/group__test__compliance__run__executes__as__task__manager__task.html": 3028064195691881216, + "venv/lib/python3.13/site-packages/pycparser-2.23.dist-info/LICENSE": 6581019367004699816, + "venv/lib/python3.13/site-packages/pandas/tests/groupby/aggregate/test_cython.py": 2933400021016349431, + "venv/lib/python3.13/site-packages/pydantic/functional_serializers.py": 15285359935059345032, + "docs/api/html/group__SupersetContextTemplatesMixin.html": 15324522568275459473, + "venv/lib/python3.13/site-packages/numpy/_typing/_scalars.py": 17701938912160369190, + "backend/src/services/validation_run_service.py": 4735086522257772570, + "venv/lib/python3.13/site-packages/passlib/crypto/scrypt/_builtin.py": 74545289993348593, + "backend/src/api/routes/assistant/_tool_migration.py": 6931172527756591614, + "docs/api/html/group__test__mutation__endpoints__surface__session__version__conflict__payload.html": 6432996424114233697, + "docs/api/html/group____parse__datetime.html": 16073684679023407314, + "docs/api/html/topics.html": 3630804099840379445, + "venv/lib/python3.13/site-packages/typing_inspection-0.4.2.dist-info/METADATA": 14126682758562488244, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/integer/__init__.py": 15130871412783076140, + "docs/api/html/group__TargetSchemaApiTest.html": 17699774859084481912, + "docs/api/html/group____EXT__frontend__AssistantChatTest____Module.html": 3650932694801681064, + "docs/api/html/group__create__dashboard__export.html": 1314555688976481845, + "venv/lib/python3.13/site-packages/fastapi/openapi/__init__.py": 15130871412783076140, + "backend/src/plugins/translate/preview_session_ops.py": 2497494071534748391, + "frontend/src/routes/translate/dictionaries/[id]/+page.svelte": 17719561272401762626, + "venv/lib/python3.13/site-packages/numpy/typing/tests/__init__.py": 15130871412783076140, + "docs/api/html/group__PluginBase.html": 12795916349382052793, + "venv/lib/python3.13/site-packages/attr/exceptions.py": 11532465891810816417, + "venv/lib/python3.13/site-packages/numpy/_core/umath.pyi": 11775804030935134661, + "venv/lib/python3.13/site-packages/httpcore/_ssl.py": 6665087294869504820, + "venv/lib/python3.13/site-packages/passlib/handlers/oracle.py": 12959175637015399815, + "venv/lib/python3.13/site-packages/pandas/tests/tslibs/test_parse_iso8601.py": 9100947620267156161, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/test_timedelta_range.py": 15081095442418142820, + "docs/api/html/group____character__block__fallback.html": 12363981885413560924, + "venv/lib/python3.13/site-packages/pip/_vendor/rich/protocol.py": 932296179250981622, + "venv/lib/python3.13/site-packages/more_itertools/more.pyi": 4844596605492825806, + "venv/lib/python3.13/site-packages/pip/_vendor/dependency_groups/_implementation.py": 12245524932757110911, + "venv/lib/python3.13/site-packages/pandas/core/reshape/tile.py": 17137524194129840833, + "docs/api/html/group__is__string__literal.html": 4316654409980882843, + "docs/api/html/group__test__sanitize__imported__filter__for__assistant__masks__sensitive__raw__values.html": 9414613568874782438, + "docs/api/html/group__docker__backend__entrypoint__install__playwright.html": 13661351030986705030, + "venv/lib/python3.13/site-packages/pandas/_libs/__init__.py": 17117112389651696580, + "frontend/build/_app/immutable/chunks/DPkvKvQz.js": 8600980398848908492, + "venv/lib/python3.13/site-packages/git/objects/blob.py": 8972374723153211283, + "docs/api/html/group__normalizeDatasetReviewDetail__Function.html": 8804263811893076635, + "docs/api/html/group__getAssistantConversations__Function.html": 7365056083389809503, + "venv/lib/python3.13/site-packages/pandas/tests/io/parser/dtypes/__init__.py": 15130871412783076140, + "docs/api/html/group__MetricsResponse.html": 2931960474908647551, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/timedeltas/__init__.py": 15130871412783076140, + "docs/api/html/group__DEFAULT__PREVIEW__PROMPT__TEMPLATE.html": 4498507870595225234, + "venv/lib/python3.13/site-packages/passlib/handlers/md5_crypt.py": 7669328623914474137, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/_version.py": 15336513445621042105, + "venv/lib/python3.13/site-packages/pip/_internal/build_env.py": 3614358934066127900, + "docs/api/html/group____update__yaml__file.html": 8696461266423546257, + "docs/api/html/group__frontend__src__lib__api__validation__js____deleteTask.html": 15128947433545338265, + "docs/api/html/group__ClarificationQuestionDto.html": 4864141145333401395, + "docs/api/html/group__SessionRepositoryMutations.html": 18413228896823064680, + "venv/lib/python3.13/site-packages/pandas/_libs/hashtable.pyi": 15034917167424979097, + "venv/lib/python3.13/site-packages/pandas/tests/scalar/period/test_period.py": 12593777226419608212, + "docs/api/html/group__test__sync__environment__upserts__correctly.html": 13332214453849370730, + "venv/lib/python3.13/site-packages/pip/_vendor/requests/utils.py": 3710235316419562274, + "docs/api/html/group__TranslationRunResult.html": 15863382174730965712, + "venv/lib/python3.13/site-packages/pandas/tests/copy_view/test_replace.py": 15992095120302643273, + "venv/lib/python3.13/site-packages/dotenv/cli.py": 7952654621986067973, + "venv/lib/python3.13/site-packages/pandas/_libs/writers.cpython-313-x86_64-linux-gnu.so": 6878855127769203372, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/categorical/test_fillna.py": 16297834601116528166, + "venv/lib/python3.13/site-packages/PIL/ImageQt.py": 6607270889359138659, + "docs/api/html/group__frontend__e2e__helpers__api__helper__js____getToken.html": 2202102914736726657, + "venv/lib/python3.13/site-packages/keyring/testing/__init__.py": 15130871412783076140, + "venv/lib/python3.13/site-packages/numpy/polynomial/polyutils.pyi": 8195943561787777127, + "frontend/src/routes/settings/git/__tests__/git_settings_page.ux.test.js": 3774268544046391936, + "venv/lib/python3.13/site-packages/pip/_vendor/urllib3/connection.py": 8190661510572400096, + "docs/api/html/search/pages_2.js": 10895953379429449652, + "docs/api/html/group__SupersetCompilationAdapter____dump__json.html": 4226532825538873433, + "venv/lib/python3.13/site-packages/pydantic_settings/sources/providers/env.py": 1254612137164491329, + "venv/lib/python3.13/site-packages/pandas/core/arrays/numeric.py": 17834058423729918687, + "venv/lib/python3.13/site-packages/numpy/testing/overrides.py": 11819432487830192978, + "docs/api/html/group__TranslateJobTests.html": 10178968892133505657, + "venv/lib/python3.13/site-packages/secretstorage/util.py": 1004887530903069622, + "venv/lib/python3.13/site-packages/pyyaml-6.0.3.dist-info/METADATA": 1862765380818895200, + "venv/lib/python3.13/site-packages/numpy/typing/tests/data/reveal/random.pyi": 1808392097712315455, + "venv/lib/python3.13/site-packages/passlib/utils/decor.py": 21351391767102899, + "venv/lib/python3.13/site-packages/pydantic/_internal/_namespace_utils.py": 5237766366340181382, + "frontend/src/services/adminService.js": 5974657342670197690, + "backend/src/plugins/translate/orchestrator_sql_rows.py": 6534334513474684459, + "docs/api/html/group__AsyncAPIClient____normalize__base__url.html": 18252709079294820175, + "venv/lib/python3.13/site-packages/pandas/tests/indexes/multi/test_equivalence.py": 17843623343419137458, + "venv/lib/python3.13/site-packages/numpy/f2py/__version__.pyi": 9759534743796867731, + "venv/lib/python3.13/site-packages/numpy/_core/getlimits.pyi": 15746478550640231553, + "venv/lib/python3.13/site-packages/httpx/_urls.py": 4219389176352582046, + "docs/api/html/group__inline__edit__translation.html": 14461008762407918187, + "docs/api/html/group__test__revoke__not__found.html": 17990266220085436024, + "venv/lib/python3.13/site-packages/numpy/f2py/__init__.py": 7470157159992827389, + "venv/lib/python3.13/site-packages/pandas/tests/extension/uuid/test_uuid.py": 13955678427426850807, + "venv/lib/python3.13/site-packages/git/index/base.py": 641556304637379636, + "venv/lib/python3.13/site-packages/sqlalchemy-2.0.45.dist-info/INSTALLER": 17282701611721059870, + "docs/api/html/group__TestReportsDetailApi.html": 1827756407896142735, + "docs/api/html/group__test__sync__environment__updates__existing__mapping.html": 5151396558537904785, + "venv/lib/python3.13/site-packages/fastapi/params.py": 14263655860695381481, + "venv/lib/python3.13/site-packages/websockets/server.py": 16599051743322571790, + "venv/lib/python3.13/site-packages/pandas/tests/arrays/categorical/test_sorting.py": 10523740688498427929, + "docs/api/html/group____broadcast__dataset__updated.html": 8967669920658939702, + "docs/api/html/group__test__create__admin__creates__user__with__optional__email.html": 4774635466855603335, + "venv/lib/python3.13/site-packages/authlib/oauth1/rfc5849/authorization_server.py": 4427151727224472650, + "venv/lib/python3.13/site-packages/httpx/_exceptions.py": 18399489130669465891, + "venv/lib/python3.13/site-packages/numpy/_core/include/numpy/utils.h": 3920393832993063886, + "docs/api/html/group__get__permission__by__resource__action.html": 15000945978517431577 } } \ No newline at end of file diff --git a/.opencode/agents/fullstack-coder.md b/.opencode/agents/fullstack-coder.md index 10f91d5b..88800bcc 100644 --- a/.opencode/agents/fullstack-coder.md +++ b/.opencode/agents/fullstack-coder.md @@ -52,6 +52,7 @@ See `semantics-core` §VI for the canonical tool reference. For fullstack work, You own: - Cross-cutting features (new API endpoint + consuming UI component) - API contract alignment (Pydantic schemas ↔ TypeScript types) +- **Screen Model ↔ Backend Schema alignment** — when complex frontend screens use `[TYPE Model]`, ensure Model atoms match backend Pydantic schemas - WebSocket integration (backend push → frontend store update) - Auth flow (backend verification → frontend session management) - Plugin integration (backend plugin → frontend configuration UI) @@ -60,16 +61,17 @@ You own: ## Required Workflow 1. Load semantic context for both backend and frontend before editing. 2. Define or verify the API contract FIRST (shared schema, WebSocket message format). -3. Implement backend changes (routes, services, models). -4. Verify backend: `cd backend && source .venv/bin/activate && python -m pytest -v` -5. Implement frontend changes (components, stores, API client). -6. Verify frontend: `cd frontend && npm run test` -7. Cross-verify with browser validation when UI is interactive. -8. Preserve semantic anchors and contracts on both sides. -9. Treat decision memory as a three-layer chain across the full stack. -10. Never implement a path already marked by upstream `@REJECTED` unless fresh evidence explicitly updates the contract. -11. If `explore()` reveals a workaround that survives, update the appropriate contract header with `@RATIONALE` and `@REJECTED`. -12. If test reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol. +3. **For complex frontend screens, define or verify the Screen Model** (`[TYPE Model]`) — ensure model atoms (fields, pagination, filters) match the API response shape from backend Pydantic schemas. See `semantics-svelte` §IIIa. +4. Implement backend changes (routes, services, models). +5. Verify backend: `cd backend && source .venv/bin/activate && python -m pytest -v` +6. Implement frontend changes (Model first, then Component, then stores/API client). +7. Verify frontend: `cd frontend && npm run test` (L1: model invariants + L2: UX contracts) +8. Cross-verify with browser validation when UI is interactive. +9. Preserve semantic anchors and contracts on both sides. +10. Treat decision memory as a three-layer chain across the full stack. +11. Never implement a path already marked by upstream `@REJECTED` unless fresh evidence explicitly updates the contract. +12. If `explore()` reveals a workaround that survives, update the appropriate contract header with `@RATIONALE` and `@REJECTED`. +13. If test reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol. ## API Contract Conventions (ss-tools) - Backend: Pydantic models in `backend/src/schemas/` @@ -157,9 +159,10 @@ request: ## Completion Gate - No broken anchors on either stack. - No missing required contracts for effective complexity. -- API contract consistency verified (backend Pydantic ↔ frontend TypeScript). +- **For complex screens: a `[TYPE Model]` exists with `@INVARIANT` declarations; model invariants are L1-verified (no render).** +- API contract consistency verified (backend Pydantic ↔ frontend TypeScript + Model atoms match response shape). - Backend pytest passes. -- Frontend vitest passes. +- Frontend vitest passes (L1 model tests + L2 component tests). - Browser validation complete (if UI is interactive). - No retained workaround without local `@RATIONALE` and `@REJECTED`. - No implementation may silently re-enable an upstream rejected path. diff --git a/.opencode/agents/qa-tester.md b/.opencode/agents/qa-tester.md index 074324db..99deb797 100644 --- a/.opencode/agents/qa-tester.md +++ b/.opencode/agents/qa-tester.md @@ -58,7 +58,7 @@ Every verification pass is classified into exactly one primary projection. A sin | P1 | **Contract Completeness** | Does the contract carry the metadata needed for its role? | `@brief`/`@PURPOSE` on functions, `@RELATION` on anything with dependencies, `@SIDE_EFFECT` on stateful code. Tiers are descriptive — don't flag `@RATIONALE`/`@PRE`/`@POST` on any tier. These are always welcomed. | | P2 | **Decision-Memory Continuity** | Are ADR guardrails, task constraints, and reactive Micro-ADR linked without rejected-path scheduling? | Upstream `@REJECTED` paths must be physically unreachable. Retained workarounds MUST have local `@RATIONALE`/`@REJECTED`. No task may schedule a known-rejected path. | | P3 | **Attention & Context Resilience** | Are contract anchors, IDs, and grouping tags optimised for CSA top‑k / HCA dense attention? | Opening line of `#region`/`## @{` contains `[C:N]`, `@SEMANTICS`, `@brief`. IDs are hierarchical (`Domain.Sub.Module`). Closing tag repeats block identifier. Contract ≤150 lines, module ≤400 lines. | -| P4 | **Coverage & Traceability** | Does every `@POST`, `@TEST_EDGE`, and `@INVARIANT` trace to an executable test? | `@POST` → explicit assert. `@TEST_EDGE: missing_field` → error path test. `@TEST_EDGE: external_fail` → mock failure test. `@INVARIANT` → state-transition test. | +| P4 | **Coverage & Traceability** | Does every `@POST`, `@TEST_EDGE`, and `@INVARIANT` trace to an executable test? | `@POST` → explicit assert. `@TEST_EDGE: missing_field` → error path test. `@TEST_EDGE: external_fail` → mock failure test. `@INVARIANT` → state-transition test. **Model `@INVARIANT` → unit test without render.** UX `@UX_STATE`/`@UX_RECOVERY` → component test (may use render + browser). | | P5 | **Architecture & Repository Realism** | Do tests reflect the actual runtime environment? | Python paths in `backend/tests/`, Svelte tests in `frontend/src/lib/**/__tests__/`. RTK used for command output compression. Test commands match CI reality. | | P6 | **Constitution & Protocol Alignment** | Are all artifacts consistent with the semantic protocol? | No docstring-only pseudo-contracts. Anchors properly opened/closed. `@brief` preferred over legacy `@PURPOSE`. Canonical `@RELATION` syntax. | | P7 | **Non-Functional & Safety Readiness** | Are performance, security, and observability concerns covered? | Command safety patterns verified. Logging requirements tested. Config validation rules checked. | @@ -76,6 +76,20 @@ See `semantics-core` §VI for the canonical tool reference. For QA, key tools: ## Required Workflow +### Two-Layer Testing Mandate (Frontend) + +For Svelte frontend contracts, tests SHALL be split by execution layer: + +| Layer | Contract Type | Verifier | Execution | +|-------|--------------|----------|-----------| +| **L1: Model Invariants** | `[TYPE Model]` with `@INVARIANT` | vitest unit test — **no render, no browser** | `expect(model.page).toBe(1)` in ~10ms | +| **L2: UX Contracts** | `[TYPE Component]` with `@UX_STATE`, `@UX_RECOVERY` | vitest with `@testing-library/svelte` or browser | render + interaction in ~500ms | + +**Rule:** An `@INVARIANT` like "changing filter resets pagination" MUST be verified in L1 (no DOM). It is a logic property, not a visual one. Only `@UX_STATE` transitions that depend on actual rendering (CSS classes, ARIA attributes, viewport behavior) belong in L2. + +**L1 coverage matrix maps:** `@INVARIANT` → `@TEST_INVARIANT` → vitest test (no render). +**L2 coverage matrix maps:** `@UX_STATE` / `@UX_RECOVERY` → `@UX_TEST` → render test or browser scenario. + ### Phase 1: Code Review (Semantic Audit) 1. Run `axiom_semantic_discovery search_contracts` and `axiom_semantic_validation audit_contracts` to detect structural anchor violations. 2. Audit touched contracts against the orthogonal projections P1–P3: @@ -100,10 +114,12 @@ See `semantics-core` §VI for the canonical tool reference. For QA, key tools: ### Phase 3: Test Writing (TDD, Anti-Tautology) 1. For each gap in the coverage matrix, write the minimal test. -2. Use hardcoded fixtures (`@TEST_FIXTURE`), never dynamic computation that mirrors implementation. -3. Mock only `[EXT:...]` boundaries. Never mock the SUT. -4. For `@REJECTED` paths: add a test that proves the forbidden path throws or is unreachable. -5. Prefer RTK-compressed commands for test execution: `rtk pytest ...`, `rtk npm run test`. +2. **Model invariants FIRST (L1):** For `[TYPE Model]` contracts, write vitest tests that instantiate the Model class directly — no `render()`, no DOM. Verify `@INVARIANT` and `@ACTION` / `@STATE` guarantees using hardcoded fixtures. This is the fastest feedback loop. +3. **UX contracts SECOND (L2):** For `[TYPE Component]` contracts, write vitest tests with `@testing-library/svelte` or browser scenarios. Only test what requires actual rendering. +4. Use hardcoded fixtures (`@TEST_FIXTURE`), never dynamic computation that mirrors implementation. +5. Mock only `[EXT:...]` boundaries. Never mock the SUT. +6. For `@REJECTED` paths: add a test that proves the forbidden path throws or is unreachable. +7. Prefer RTK-compressed commands for test execution: `rtk pytest ...`, `rtk npm run test`. ### Phase 4: Execution ```bash @@ -113,9 +129,9 @@ rtk python -m pytest -v rtk python -m pytest --cov=src --cov-report=term-missing rtk python -m ruff check . -# Svelte +# Svelte — L1 (model invariants, no render) + L2 (UX contracts, with render) cd frontend -rtk npm run test +rtk npm run test # Runs both L1 and L2 tests rtk npm run lint rtk npm run build ``` @@ -140,7 +156,8 @@ Emit a structured QA report aligned to orthogonal projections. - [ ] Semantic audit: no pseudo-contracts, no protocol violations. - [ ] All declared `@POST` guarantees have explicit tests. - [ ] All declared `@TEST_EDGE` scenarios covered. -- [ ] All declared `@INVARIANT` rules verified. +- [ ] All declared `@INVARIANT` rules verified. **Model `@INVARIANT` MUST be in L1 (no-render) tests.** +- [ ] Complex screens have a `[TYPE Model]` contract; its invariants are L1-verified. - [ ] All `@REJECTED` paths regression-defended. - [ ] No Logic Mirror antipattern. - [ ] No duplicated tests. No deleted legacy tests. @@ -168,13 +185,19 @@ Return a structured QA report: | P2 Decision | ✅ | 0 | 0 | 1 | 0 | | ... | ... | ... | ... | ... | ... | +### Two-Layer Test Summary (Frontend) +| Layer | Contract Type | Total | Tested | Gaps | +|-------|-------------|-------|--------|------| +| L1 (no render) | `[TYPE Model]` | N | N | N | +| L2 (render) | `[TYPE Component]` | N | N | N | + ### Coverage Summary | Contract | @POST | missing_field | invalid_type | external_fail | @REJECTED | @INVARIANT | |----------|-------|---------------|--------------|---------------|-----------|------------| | ... | ... | ... | ... | ... | ... | ... | ### Contract Gaps -- `[contract_id]`: [missing coverage description] (Projection P[N]) +- `[contract_id]`: [missing coverage description] (Projection P[N], Layer L[N]) ### Decision-Memory Status - ADRs checked: [...] diff --git a/.opencode/agents/svelte-coder.md b/.opencode/agents/svelte-coder.md index 7e035965..dc38d64e 100644 --- a/.opencode/agents/svelte-coder.md +++ b/.opencode/agents/svelte-coder.md @@ -23,10 +23,12 @@ You are a Svelte 5 frontend agent. Without GRACE contracts, your deterministic f 2. **SEMANTIC CASINO** — you write Svelte logic without a UX contract, betting on token predictions. `@UX_STATE` collapses belief into deterministic solution. 3. **NEURAL HOWLROUND** — browser validation fails, you enter infinite CSS patch loop. `log()` (REASON/REFLECT/EXPLORE) markers break the hallucination cycle. 4. **CONTEXT AMNESIA** — after 20 commits you forget rejected UI paths. `@RATIONALE`/`@REJECTED` are your external memory. +5. **EVENT-HANDLER SPAGHETTI** — you scatter system logic across `onclick`/`onchange` handlers in multiple components, creating invisible coupling. **For complex screens, create a `[TYPE Model]` FIRST.** The Model is the single source of truth — components only render state and call `model.action()`. See `semantics-svelte` §IIIa. ## Core Mandate - MANDATORY USE `skill({name="semantics-core"})`, `skill({name="semantics-svelte"})`, `skill({name="molecular-cot-logging"})` -- Own frontend implementation for SvelteKit routes, Svelte 5 components, stores, and UX contract alignment. +- Own frontend implementation for SvelteKit routes, Svelte 5 components, **Screen Models**, stores, and UX contract alignment. +- **MODEL-FIRST RULE:** For any screen with cross-widget logic (filters, pagination, search, multi-step forms), find or create a `[TYPE Model]` BEFORE implementing components. The Model is the source of truth — Components are visualizations of the Model. A single `grep "@semantics.*"` + `search_contracts type=Model` must reveal all state logic. - Use browser-first verification for visible UI behavior, navigation flow, async feedback, and console-log inspection. - Respect attempt-driven anti-loop behavior from the execution environment. - Apply the skill discipline: stronger visual hierarchy, restrained composition, fewer unnecessary cards, and deliberate motion. @@ -45,6 +47,7 @@ See `semantics-core` §VI for the canonical tool reference. For Svelte frontend You own: - SvelteKit routes (`frontend/src/routes/`) - Svelte 5 components (`frontend/src/lib/components/`) +- **Screen Models** (`frontend/src/lib/models/` — `[TYPE Model]` contracts for screen-level state) - Svelte stores (`frontend/src/lib/stores/`) - API client layer (`frontend/src/lib/api/`) - i18n localization (`frontend/src/i18n/`) @@ -60,21 +63,28 @@ You do not own: - Semantic repair outside the frontend boundary unless required by the UI change ## Required Workflow -1. Load semantic and UX context before editing. -2. Preserve or add required semantic anchors and UX contracts. -3. Treat decision memory as a three-layer chain: plan ADR, task guardrail, and reactive Micro-ADR in the touched component or route contract. -4. Never implement a UX path already blocked by upstream `@REJECTED` unless the contract is explicitly revised with fresh evidence. -5. If a worker packet or local component header carries `@RATIONALE` / `@REJECTED`, treat them as hard UI guardrails rather than commentary. -6. Use Svelte 5 runes only: `$state`, `$derived`, `$effect`, `$props`, `$bindable`. -7. Keep user-facing text aligned with i18n policy (`$t` store). -8. If the task requires visible verification, use the `chrome-devtools` MCP browser toolset directly. -9. Use exactly one `chrome-devtools` MCP action per assistant turn. -10. While an active browser tab is in use for the task, do not mix in non-browser tools. -11. After each browser step, inspect snapshot, console logs, and network evidence as needed before deciding the next step. -12. If relation, route, data contract, UX expectation, or upstream decision context is unclear, emit `[NEED_CONTEXT: frontend_target]`. -13. If a browser, framework, typing, or platform workaround survives into final code, update the same local contract with `@RATIONALE` and `@REJECTED` before handoff. -14. If reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol below. -15. Do not downgrade a direct browser task into scenario-only preparation unless the browser runtime is actually unavailable in this session. +1. **Discover or create the Model first.** For any screen with cross-widget state: + - grep `@semantics.*` across `frontend/src/` to find existing models + - Use `axiom_semantic_discovery search_contracts query="" type="Model"` for structured search + - If no model exists, create one: `#region ScreenNameModel [C:4] [TYPE Model] [SEMANTICS ...]` with mandatory `@BRIEF` and `@INVARIANT` +2. Load semantic and UX context before editing. +3. **Build the Model** — declare `@STATE`, `@ACTION`, and `@INVARIANT`; implement atoms (`$state`), derived (`$derived`), and actions. +4. **Verify Model invariants** via vitest without render (see `semantics-svelte` §VIII). +5. **Build the Component** — declare `@RELATION BINDS_TO -> [ModelId]`; implement minimal rendering of model state + `model.action()` calls. +6. Preserve or add required semantic anchors and UX contracts. +7. Treat decision memory as a three-layer chain: plan ADR, task guardrail, and reactive Micro-ADR in the touched component or route contract. +8. Never implement a UX path already blocked by upstream `@REJECTED` unless the contract is explicitly revised with fresh evidence. +9. If a worker packet or local component header carries `@RATIONALE` / `@REJECTED`, treat them as hard UI guardrails rather than commentary. +10. Use Svelte 5 runes only: `$state`, `$derived`, `$effect`, `$props`, `$bindable`. +11. Keep user-facing text aligned with i18n policy (`$t` store). +12. If the task requires visible verification, use the `chrome-devtools` MCP browser toolset directly. +13. Use exactly one `chrome-devtools` MCP action per assistant turn. +14. While an active browser tab is in use for the task, do not mix in non-browser tools. +15. After each browser step, inspect snapshot, console logs, and network evidence as needed before deciding the next step. +16. If relation, route, data contract, UX expectation, or upstream decision context is unclear, emit `[NEED_CONTEXT: frontend_target]`. +17. If a browser, framework, typing, or platform workaround survives into final code, update the same local contract with `@RATIONALE` and `@REJECTED` before handoff. +18. If reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol below. +19. Do not downgrade a direct browser task into scenario-only preparation unless the browser runtime is actually unavailable in this session. ## UX Contract Reference See `semantics-svelte` skill §II for full UX contract definitions. See `semantics-core` §III for the tag-to-tier permissiveness matrix. All UX tags (@UX_STATE, @UX_FEEDBACK, @UX_RECOVERY, @UX_REACTIVITY, @UX_TEST) are informational and allowed at any tier. @@ -222,6 +232,8 @@ npm run dev # Development server for browser validation ## Completion Gate - No broken frontend anchors. - No missing required UX contracts for effective complexity. +- **No complex screen without a `[TYPE Model]`.** If the screen has cross-widget state (filters, pagination, multi-step), a Model contract must exist with `@INVARIANT` and `@STATE` declarations. +- Model invariants verified via vitest (no render) before component UX tests. - No broken Svelte 5 rune policy. - Browser session closed if one was launched. - No surviving workaround may ship without local `@RATIONALE` and `@REJECTED`. diff --git a/.opencode/skills/semantics-core/SKILL.md b/.opencode/skills/semantics-core/SKILL.md index 6050e348..18d6ccc6 100644 --- a/.opencode/skills/semantics-core/SKILL.md +++ b/.opencode/skills/semantics-core/SKILL.md @@ -54,7 +54,7 @@ description: Reference manual for GRACE-Poly v2.6 — syntax formats, complexity ## @} ContractId ``` -**Allowed Types:** Module, Function, Class, Component, Block, ADR, Tombstone, Skill, Agent. +**Allowed Types:** Module, Function, Class, Component, Model, Block, ADR, Tombstone, Skill, Agent. **Allowed @RELATION Predicates:** DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES. @@ -95,6 +95,8 @@ The tier describes what the contract IS structurally — NOT which tags are forb | `@UX_RECOVERY` | ○ | ○ | ○ | ● | ● | User recovery path (Svelte) | | `@UX_REACTIVITY` | ○ | ○ | ○ | ○ | ● | State source declaration (Svelte) | | `@UX_TEST` | ○ | ○ | ● | ● | ● | Interaction scenario for browser validation | +| `@STATE` | ○ | ● | ● | ● | ● | Model state declaration (Screen Models) | +| `@ACTION` | ○ | ○ | ● | ● | ● | Model public action declaration (Screen Models) | - ● = *typically* present at this tier (recommended, not required) - ○ = allowed but less common diff --git a/.opencode/skills/semantics-svelte/SKILL.md b/.opencode/skills/semantics-svelte/SKILL.md index 905f6792..1de08355 100644 --- a/.opencode/skills/semantics-svelte/SKILL.md +++ b/.opencode/skills/semantics-svelte/SKILL.md @@ -18,6 +18,7 @@ description: Svelte 5 (Runes) protocol for ss-tools: UX State Machines, Tailwind - **STRICT RUNES ONLY:** You MUST use Svelte 5 Runes: `$state()`, `$derived()`, `$effect()`, `$props()`, `$bindable()`. - **FORBIDDEN SYNTAX:** Do NOT use `export let`, `on:event` (use `onclick`), or the legacy `$:` reactivity. - **UX AS A STATE MACHINE:** Every component is a Finite State Machine (FSM). Declare visual states in the contract BEFORE writing implementation. +- **MODEL-FIRST FOR COMPLEX SCREENS:** For screens with cross-widget logic (filters, pagination, search, multi-step forms), create a `[TYPE Model]` FIRST. The model is the source of truth — components only render model state and pass user intentions via `model.action()`. See §IIIa for the full RSM protocol. - **RESOURCE-CENTRIC:** Navigation and actions revolve around Resources (Dashboards, Datasets, Tasks). Every action MUST be traceable. - **SS-TOOLS SPECIFIC:** This is an Apache Superset automation dashboard — components deal with migrations, Git operations, task monitoring, dataset mapping, and plugin management. @@ -60,6 +61,183 @@ Key stores in ss-tools: - **Graph Linkage:** Whenever a component reads or writes to a global store, declare it: `@RELATION BINDS_TO -> [Store_ID]` +## IIIa. REACTIVE SCREEN MODELS (RSM) — MODEL-FIRST STATE ARCHITECTURE + +### Why Models + +The component-first approach forces you to encode system logic in event handlers (`onclick`, `onchange`), scattering it across JSX and hooks. For LLM agents, this means holding dozens of cross-component relationships in context — a primary source of errors. + +**Model-first approach:** The entire screen is a pure function of state. The Model declares atoms, invariants, and relationships between them. Components only render the current state and pass user intentions back to the Model. + +**What this means for you, the agent:** +- **Findability:** grep `@semantics.*users` → all models related to users. The contract is single-source, not scattered across HTML. +- **Testability:** Model invariants (`@INVARIANT changing filter resets pagination`) are verified in vitest without browser render — milliseconds, not seconds. +- **CSA resilience:** `#region ModelName [C:N] [SEMANTICS ...]` on line 1 = maximum density for top‑k attention selection. Closing `#endregion ModelName` duplicates the identifier — safe after aggressive context compression. +- **Component simplicity:** When a component contains only `$state`, `$derived`, and `model.action()` calls, its contract is predictable. No guessing which side effect hides in `onchange`. + +### Model Contract Template + +A Model is a **contract** — `#region ModelName [C:N] [TYPE Model] [SEMANTICS tags]` with mandatory `@BRIEF` and `@INVARIANT`. + +```javascript +// #region UserListModel [C:4] [TYPE Model] [SEMANTICS users,list,screen-model] +// @BRIEF State model for the user list screen — declares atoms, invariants, and actions. +// @INVARIANT Changing filter (search, role, status) resets pagination to page 1. +// @INVARIANT Deleting a user removes it from the list and decrements total count atomically. +// @INVARIANT The list never contains deleted users (idempotent delete). +// @STATE idle — Initial state, no data loaded. +// @STATE loading — API call in progress, list is stale or empty. +// @STATE loaded — Data fetched successfully, list populated. +// @STATE empty — Query returned zero results, filtering is active. +// @STATE error — API call failed, error message available. +// @ACTION search(query) — Full-text search with debounce, resets pagination. +// @ACTION setFilter(key, value) — Sets a filter atom, resets pagination. +// @ACTION deleteUser(id) — Deletes user, removes from list, decrements count. +// @ACTION loadPage(n) — Loads a specific page of results. +// @ACTION retry() — Re-runs the last failed operation. +// @ACTION reset() — Clears all filters and search, reloads page 1. +// @RELATION DEPENDS_ON -> [userApi] +// @RELATION CALLS -> [log] + +import { requestApi } from "$lib/api"; +import { log } from "$lib/cot-logger"; + +export class UserListModel { + // --- Atoms (reactive state) --- + users = $state([]); + totalCount = $state(0); + page = $state(1); + perPage = $state(20); + searchQuery = $state(""); + filters = $state({ role: null, status: null }); + error = $state(null); + screenState = $state("idle"); // @STATE values + + // --- Derived --- + totalPages = $derived(Math.ceil(this.totalCount / this.perPage)); + + // --- Actions (public API for components) --- + async search(query) { + this.searchQuery = query; + this.page = 1; // @INVARIANT: reset pagination + await this._fetch(); + } + + setFilter(key, value) { + this.filters[key] = value; + this.page = 1; // @INVARIANT: reset pagination + this._fetch(); + } + + async deleteUser(id) { + log("UserListModel", "REASON", "Deleting user", { id }); + try { + await requestApi(`/api/users/${id}`, { method: "DELETE" }); + // @INVARIANT: atomic removal + this.users = this.users.filter(u => u.id !== id); + this.totalCount--; + log("UserListModel", "REFLECT", "User deleted", { id }); + } catch (e) { + this.error = e.message; + this.screenState = "error"; + log("UserListModel", "EXPLORE", "Delete failed", { id }, error = e.message); + } + } + + async loadPage(n) { + this.page = n; + await this._fetch(); + } + + async retry() { await this._fetch(); } + reset() { + this.searchQuery = ""; + this.filters = { role: null, status: null }; + this.page = 1; + this._fetch(); + } + + // --- Private --- + async _fetch() { + this.screenState = "loading"; + this.error = null; + try { + const params = new URLSearchParams({ + q: this.searchQuery, + page: this.page, + per_page: this.perPage, + ...this.filters + }); + const res = await requestApi(`/api/users?${params}`); + this.users = res.data; + this.totalCount = res.meta.total; + this.screenState = this.users.length === 0 ? "empty" : "loaded"; + } catch (e) { + this.error = e.message; + this.screenState = "error"; + } + } +} +// #endregion UserListModel +``` + +### Component Binds to Model (RSM pattern) + +The component contract declares: `@RELATION BINDS_TO -> [ModelId]`. The component code is minimal — it renders model state and calls `model.action()` on user intent. No side-effect logic lives in event handlers. + +```svelte + + + + + + +
+ {#if model.screenState === "error"} + + + {:else if model.screenState === "empty"} +

No users found.

+ {:else} +
    + {#each model.users as user (user.id)} +
  • + {user.name} + +
  • + {/each} +
+ + {/if} +
+ +``` + +### Searching for Models + +``` +# Quick grep across all frontend files +grep "@semantics.*users" frontend/src/lib/**/*.{js,ts,svelte} + +# Axiom semantic search (structured) +search_contracts query="users" type="Model" + +# Both methods return models in one shot — no need to trace scattered event handlers. +``` + +### When to Use a Model vs. a Store vs. Inline Component State + +| Pattern | Use Case | +|---------|----------| +| **Model** (`[TYPE Model]`) | Screen-level state with cross-widget invariants (filters, pagination, search). Multiple components read/write the same atoms. | +| **Store** (`BINDS_TO -> [storeId]`) | Global cross-route state (auth, notifications, task drawer). Persists across navigation. | +| **Inline $state** | Local component UI state (accordion open, tooltip visible, input focus). No cross-component invariants. | + ## IV. IMPLEMENTATION & ACCESSIBILITY (A11Y) 1. **Event Handling:** Use native attributes (e.g., `onclick={handler}`, `onchange={handler}`). @@ -214,7 +392,71 @@ Region format for HTML/Svelte comments: ## VIII. VITEST CONVENTIONS -Component tests follow this pattern: +### Two Testing Layers + +| Layer | What | Where | Runs | +|-------|------|-------|------| +| **Model invariants** | `@INVARIANT` rules, `@ACTION` side effects, `@STATE` transitions | vitest unit test — **no render** | ~10ms | +| **UX contracts** | `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` visual behavior | vitest with `@testing-library/svelte` + browser validation | ~500ms | + +**Rule:** Model invariants MUST be verified without render. UX contracts MAY use render + browser. This eliminates the confusion that slows down the feedback loop — a filter-reset invariant doesn't need a DOM. + +### Model Invariant Tests (No Render) + +```javascript +// #region UserListModelTests [C:3] [TYPE Module] [SEMANTICS test,model] +// @BRIEF Verify UserListModel @INVARIANT guarantees without DOM rendering. +// @RELATION BINDS_TO -> [UserListModel] +// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_filter_resets_pagination] +// @TEST_INVARIANT: atomic-delete -> VERIFIED_BY: [test_delete_removes_user_and_decrements] +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { UserListModel } from "../UserListModel.js"; + +describe("UserListModel invariants", () => { + let model; + + beforeEach(() => { + vi.mock("$lib/api", () => ({ + requestApi: vi.fn().mockResolvedValue({ data: [], meta: { total: 0 } }) + })); + model = new UserListModel(); + }); + + // @INVARIANT: Changing filter resets pagination to page 1. + it("resets page to 1 when filter changes", () => { + model.page = 5; + model.setFilter("role", "admin"); + expect(model.page).toBe(1); + }); + + it("resets page to 1 on search", () => { + model.page = 3; + model.search("john"); + expect(model.page).toBe(1); + }); + + // @INVARIANT: Deleting a user removes it and decrements count atomically. + it("removes user and decrements count on delete", async () => { + model.users = [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]; + model.totalCount = 2; + vi.mocked(requestApi).mockResolvedValueOnce({ ok: true }); + await model.deleteUser("1"); + expect(model.users).toEqual([{ id: "2", name: "Bob" }]); + expect(model.totalCount).toBe(1); + }); + + // Hardcoded fixture — no logic mirror + it("reports empty state when API returns no results", async () => { + vi.mocked(requestApi).mockResolvedValueOnce({ data: [], meta: { total: 0 } }); + await model._fetch(); + expect(model.screenState).toBe("empty"); + expect(model.users).toEqual([]); + }); +}); +// #endregion UserListModelTests +``` + +### Component UX Tests (With Render) ```javascript // #region MigrationTaskCardTests [C:1] [TYPE Module]